Heimdall Docs

Lazy Loading

Heimdall supports incremental rendering by letting the server return the next chunk of HTML only when a boundary becomes visible. This keeps the browser simple and keeps pagination or continuation state on the server-driven HTML boundary.

The key idea is that the DOM can carry the continuation state needed for the next request.

1. What Lazy Loading Means in Heimdall

Instead of loading everything up front, Heimdall can render an initial set of items and place a sentinel boundary at the end. When that sentinel becomes visible, it triggers the next request.

Initial items
+ sentinel row with continuation state
-> sentinel becomes visible
-> next chunk loads
-> new sentinel is rendered if more data remains

2. The Core Pattern

A Heimdall lazy-loading flow usually combines a visible trigger, closest-state or keyed closest-state payload binding, and an outer swap of the sentinel boundary.

sentinel.Heimdall()
    .Visible("Weather.LoadMore")
    .PayloadFromClosestState("weather")
    .Target("#weather-sentinel")
    .SwapOuter();

3. Why the Sentinel Matters

The sentinel is more than a marker. It is a stateful continuation boundary. It tells Heimdall when to ask for more, and it can carry the state needed to compute what more means.

It marks the current end of rendered content.
It becomes visible when the user scrolls near it.
It carries continuation state for the next request.
It is usually replaced with the next chunk plus a new sentinel.

4. Keyed State Is a Natural Fit

Lazy loading often works best with keyed state so the continuation boundary can carry only the paging or cursor information relevant to that stream.

Strongly Typed State

sentinel.Heimdall().State("weather", new WeatherState
{
    Page = 2
});

Rendered HTML Mental Model

<div
  id="weather-sentinel"
  data-heimdall-state-weather='{"page":2}'>
  ...
</div>

5. Triggering on Visibility

The visible trigger lets the browser wait until a boundary enters view before requesting more content.

sentinel.Heimdall()
    .Visible("Weather.LoadMore")
    .PayloadFromClosestState("weather")
    .Target("#weather-sentinel")
    .SwapOuter();

6. Why Outer Swap Is Usually the Right Choice

Lazy loading typically replaces the sentinel itself with a larger block: the newly loaded items plus the next sentinel if more data remains. Outer swap makes that natural.

sentinel.Heimdall()
    .Visible("Weather.LoadMore")
    .PayloadFromClosestState("weather")
    .Target("#weather-sentinel")
    .SwapOuter();

7. Full Mental Model

A lazy-loading response usually returns three things together: the next items, the updated continuation boundary, and no client-side paging logic beyond what the attributes already express.

[Initial rows...]

<div
  id="weather-sentinel"
  data-heimdall-state-weather='{"page":2}'
  heimdall-visible="Weather.LoadMore"
  heimdall-payload="closest-state:weather"
  heimdall-target="#weather-sentinel"
  heimdall-swap="outer">
  Loading more...
</div>

8. Action DTO

The server action receives the continuation state as a normal DTO.

public sealed class WeatherState
{
    public int Page { get; set; }
}

9. Action

The action uses the current continuation state to render the next chunk of UI. If more data remains, it includes a new sentinel with updated state.

[ContentInvocation]
public static IHtmlContent LoadMore(WeatherState state)
{
    var nextPage = state.Page + 1;

    return FluentHtml.Fragment(f =>
    {
        f.Add(RenderWeatherRows(state.Page));

        if (HasMore(nextPage))
        {
            f.Add(RenderWeatherSentinel(nextPage));
        }
    });
}

10. The Loop

Lazy loading in Heimdall is just a repeated server-rendering loop driven by visibility.

1. Server renders initial content + sentinel
2. Sentinel becomes visible
3. Browser sends continuation state
4. Server returns next rows + next sentinel
5. The cycle repeats until there is no next sentinel

11. Why This Works Well

This pattern stays simple because the browser does not need its own paging engine. The continuation boundary is already in the rendered HTML.

The server owns what the next chunk is.
The DOM carries the current continuation boundary.
The browser only needs to observe visibility and send the next request.
The rendered HTML remains the source of current UI truth.

12. When to Use Lazy Loading

This pattern works well when the user may never need the full dataset immediately and when the next chunk can be expressed as a continuation from the current boundary.

Good fits:
- feeds
- tables
- activity streams
- dashboards
- search results
- timeline-like UI

13. Lazy Loading vs Polling or SSE

Lazy loading is about fetching the next chunk when the user reaches a boundary. It is not the same as polling or real-time streaming.

Lazy Loading:
- user scrolls to boundary
- request next chunk

Polling:
- browser asks on a timer

SSE:
- server pushes when ready

14. Common Pattern: Rows + Sentinel

One of the cleanest implementations is to render ordinary rows followed by one last row or block that acts as the continuation sentinel.

<tbody>
  <tr>...</tr>
  <tr>...</tr>
  <tr id="weather-sentinel">Loading more...</tr>
</tbody>

Next Steps

Once lazy loading makes sense, the remaining pieces are about discovery and reference: triggers, payloads, runtime behavior, and reusable patterns.