Streaming includes

By default, includes resolve inline — the response waits until every include is ready before the first byte reaches the client. That's fine for fast includes (cache hits, simple resolvers). It's wasteful when one slow include blocks an otherwise fast page.

loading="lazy" decouples the include from the initial response. bext sends a placeholder in the shell, starts resolving the include in parallel, and streams the final content as a later chunk.

<bext-include src="/partials/recommendations"
  loading="lazy"
>Loading recommendations...</bext-include>

- Initial response: <div data-bext-include-id="inc_1">Loading recommendations...</div>

- Later chunk: <script>/* swap placeholder with resolved HTML */</script>

No framework dependency. No React. Pure HTML + a tiny inline script.

When to use lazy loading

Lazy is right when:

- The include is slow (upstream API, V8 render, fragment cache miss).

- The include is below the fold (recommendations sidebar, related posts, live feeds).

- The include is non-critical (can arrive a second later without hurting the page).

Eager (default) is right when:

- The include is in the initial viewport (nav, hero, primary content).

- The include changes page layout when it arrives (would cause CLS).

- The include is required for SEO (crawlers that don't execute JS).

How lazy streaming works

The include pipeline runs in two phases:

Phase 1: Initial HTML pass
  - Eager includes resolve inline (current behavior)
  - Lazy includes emit placeholders, register with streaming pipeline

Phase 2: Streaming flush
  - Initial shell is sent to the client
  - Pending lazy includes resolve concurrently
  - As each resolves, emit a <script> chunk that swaps the placeholder
  - Final completion chunk

The placeholder carries the include's source, so the client knows what was resolved:

<div data-bext-include-id="inc_1f3"
     data-bext-include-src="/partials/recommendations">
  Loading recommendations...
</div>

When the include resolves, bext emits:

<script>(function(){
var el = document.querySelector('[data-bext-include-id="inc_1f3"]');
if (!el) return;
var t = document.createElement('template');
t.innerHTML = "<section>...resolved HTML...</section>";
el.replaceWith(t.content);
})();</script>

This uses the same streaming pipeline as React Suspense — chunks are delivered in priority order, compressed per-chunk, and the completion chunk ends the response.

Fallback content

The content between open and close tags is the placeholder:

<bext-include src="/partials/comments" loading="lazy">
  <div class="skeleton-loader"></div>
</bext-include>

If the JavaScript swap never runs (disabled JS, bot, rendering error), the skeleton loader stays visible. Always provide meaningful fallback content for lazy includes.

Concurrent resolution

Multiple lazy includes resolve in parallel:

<bext-include src="/api/feed-a" loading="lazy">Loading A...</bext-include>
<bext-include src="/api/feed-b" loading="lazy">Loading B...</bext-include>
<bext-include src="/api/feed-c" loading="lazy">Loading C...</bext-include>

All three resolve concurrently. The page doesn't wait for the slowest; chunks arrive as each completes.

Order is not guaranteed — chunks are flushed in completion order. If you need deterministic ordering, use loading="eager".

Forcing synchronous resolution

Crawlers and non-JS clients should see full content, not placeholders. bext honors the X-Bext-Include-Mode: sync request header:

X-Bext-Include-Mode: sync

This forces every loading="lazy" include to resolve inline — the response blocks until everything is ready, but crawlers see complete HTML.

bext auto-detects common crawler user agents and injects this header automatically. You can also set it via a custom header or query param for debugging.

Streaming with caching

Lazy and cached work together. The first request pays the resolution cost; subsequent requests hit the cache and — if the cached fragment is small enough — skip lazy entirely:

<bext-include src="/partials/trending"
  loading="lazy"
  ttl="5m"
></bext-include>

bext's streaming optimizer can choose to inline the content on a cache hit (since it's already resolved) rather than streaming a placeholder. This avoids the unnecessary script chunk when the data is already available.

Progressive enhancement

Lazy includes are progressive enhancement-friendly:

- JS enabled: placeholder → streamed content via inline script.

- JS disabled: placeholder stays (meaningful fallback).

- Slow connection: placeholder visible until network catches up.

- Crawler: synchronous mode, full content in initial HTML.

No bundled client-side library. The swap script is a few inline bytes per include.

Per-chunk compression

bext's streaming pipeline compresses each chunk independently. Small chunks (< 256 bytes) skip compression to avoid overhead; larger chunks get gzip/brotli/zstd based on the client's Accept-Encoding.

Shell: ~2KB → brotli compressed
Lazy chunk: ~5KB → brotli compressed
Completion: ~20B → uncompressed

Each chunk fits into a TCP segment when possible, minimizing TTFB for the shell while keeping the full response transfer-size efficient.

Error handling

If a lazy include fails to resolve:

- With onerror="continue": silently drops (placeholder stays).

- With fallback attribute: tries the fallback source, streams that.

- With inline fallback content: the placeholder content (which already showed) stays visible.

- Otherwise: a console error is logged; placeholder stays.

The shell has already been sent — bext can't retroactively fail the response. It can only update the chunk.

Interaction with other features

Lazy works alongside all other include attributes:

<bext-include
  src="/partials/personalized"
  loading="lazy"
  auth="required"
  vary="role"
  ttl="5m"
  timeout="500ms"
  fallback="/_partials/default.html"
>Loading...</bext-include>

- Auth check happens before the placeholder is emitted (no wasted streaming).

- Cache lookup happens on resolution, not on shell emission.

- Timeout applies to the background resolution.

- Fallback chain runs if the primary source fails.

See also

- Caching includes — pair lazy with TTL for fast subsequent loads

- Timeout & Fallback — handling slow or failing includes

- Islands Architecture — for interactive components

- Compression — per-chunk compression details