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 remains2. 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.
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 sentinel11. 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.
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 UI13. 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 ready14. 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.