HTMX Lazy Loading

Lazy loading delays the fetching of content until it is actually needed. Instead of loading everything when the page first opens, you load only the essential parts immediately and defer the rest. This makes the initial page appear faster, which improves the user's experience and reduces server load on every page visit.

The Problem With Loading Everything at Once

  Traditional page load:

  Browser requests page
       |
       v
  Server builds ALL widgets:
    - Header stats
    - Sales chart
    - Recent orders table
    - User activity feed
    - Recommendation panel
       |
       v
  One big slow response → user waits 3+ seconds
       |
       v
  Page appears — but user only looks at header stats first
  With lazy loading:

  Browser requests page
       |
       v
  Server builds only the page shell (header, layout)
       |
       v
  Fast response → page appears in 0.4 seconds
       |
       v
  Each widget loads itself independently in the background
  User sees content fill in progressively

The Core Pattern: hx-trigger="load"

Place hx-trigger="load" on a div. The moment that div appears in the browser, HTMX sends a request to fetch its content. The div shows a placeholder until the response arrives.

<div
  hx-get="/dashboard/stats"
  hx-trigger="load"
  hx-target="this"
  hx-swap="innerHTML">
  <p>Loading stats...</p>
</div>

The page renders instantly with "Loading stats..." visible. HTMX fires the GET request in the background. When the server responds, the placeholder text is replaced with the real stats.

Full Dashboard Example

<!-- Page shell loads instantly -->
<h2>Dashboard</h2>

<!-- Each widget loads itself -->
<div hx-get="/widgets/revenue"    hx-trigger="load" hx-target="this">
  <span class="htmx-indicator">Loading revenue...</span>
</div>

<div hx-get="/widgets/orders"     hx-trigger="load" hx-target="this">
  <span class="htmx-indicator">Loading orders...</span>
</div>

<div hx-get="/widgets/activity"   hx-trigger="load" hx-target="this">
  <span class="htmx-indicator">Loading activity...</span>
</div>
  Timeline:

  0.0s  Page shell renders — user sees the dashboard layout
  0.1s  All three widget requests fire simultaneously
  0.4s  Revenue widget responds — fills with chart
  0.6s  Orders widget responds — fills with table
  1.2s  Activity widget responds — fills with feed

  User sees content appearing progressively.
  No single long wait blocks the page.

Skeleton Placeholders

A skeleton placeholder mimics the shape of the content that is loading. It is better than plain "Loading..." text because users can see roughly where content will appear.

<style>
  .skeleton {
    background: linear-gradient(90deg, #eee 25%, #ddd 50%, #eee 75%);
    background-size: 200% 100%;
    animation: shimmer 1.2s infinite;
    border-radius: 4px;
    height: 20px;
    margin-bottom: 8px;
  }
  @keyframes shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
  }
</style>

<div hx-get="/user/profile" hx-trigger="load" hx-target="this">
  <!-- Skeleton while loading -->
  <div class="skeleton" style="width:60%"></div>
  <div class="skeleton" style="width:80%"></div>
  <div class="skeleton" style="width:40%"></div>
</div>

Lazy Loading on Scroll: hx-trigger="revealed"

The revealed trigger fires when an element scrolls into the visible viewport. This is ideal for content below the fold — content the user has not scrolled to yet.

<section>
  <h3>Related Articles</h3>
  <div
    hx-get="/related-articles"
    hx-trigger="revealed"
    hx-target="this"
    hx-swap="outerHTML">
    <p>Scroll down to load related articles...</p>
  </div>
</section>
  Diagram:

  ┌──────────────────────────────┐  ← visible viewport
  │  Main article content here   │
  │                              │
  │  [ Related articles div ]    │  ← not visible yet
  └──────────────────────────────┘

  User scrolls down...

  ┌──────────────────────────────┐
  │  ...end of main article      │
  │                              │
  │  [ Related articles div ]    │  ← now revealed in viewport
  └──────────────────────────────┘
       |
       v
  HTMX fires GET /related-articles
  Div fills with actual article list

Load Once vs Load Every Time

Use the once trigger modifier when content should load only once. Without it, scrolling away and back into view fires the request again:

<!-- Fires every time the element enters the viewport -->
<div hx-get="/promo" hx-trigger="revealed"></div>

<!-- Fires only the first time -->
<div hx-get="/promo" hx-trigger="revealed once"></div>

For static content that does not change between views, always use once to avoid unnecessary server requests.

Lazy Loading Images

HTML already has a native loading="lazy" attribute for images. For more complex scenarios — like generating a chart image server-side or showing a placeholder graphic until a high-res image is ready — HTMX gives you more control:

<div
  hx-get="/generate-chart?id=42"
  hx-trigger="revealed once"
  hx-target="this"
  hx-swap="innerHTML">
  <img src="/placeholder-chart.png" alt="Chart loading...">
</div>

When the div enters the viewport, HTMX requests the real chart from the server. The server generates it (which may take a moment) and returns an <img> tag pointing to the finished chart. HTMX swaps the placeholder with the real chart.

Server Considerations

Each lazy-loaded widget hits the server independently. Your server routes for lazy content should be fast. If a widget is slow — because it queries a database or calls an external API — consider caching its output on the server side. Cached widgets respond in milliseconds, making the lazy load feel instant even on the first visit.

Key Takeaway

Lazy loading in HTMX uses hx-trigger="load" to fire requests when an element appears in the DOM, and hx-trigger="revealed" to fire when an element scrolls into the viewport. Both patterns make the initial page load fast by deferring non-critical content. Add the once modifier to avoid repeated requests. Use skeleton placeholders to give users a sense of structure while content loads. The result is a page that feels fast even when it contains a lot of data.

Leave a Comment

Your email address will not be published. Required fields are marked *