Leaving slide mode.

It's Time for Client-Side Includes

<template src>

Noam Rosenthal & Barry Pollard
IRC: #dpu-next

TPAC 2026
Dublin, Ireland & online
26–30 October 2026

Explainer: https://github.com/WICG/declarative-partial-updates/blob/main/fragment-include-explainer.md. Covers extending <template> with src, buffer, and sanitize to solve WHATWG HTML issue #2791.

30 Years of Trying

Including one piece of HTML inside another is one of the oldest requests on the web. SSI and PHP were invented for it in 1995. On the client, <iframe seamless> had heavy Window/Document overhead and style/security issues, and HTML Imports loaded an inert document that still needed JS to stamp DOM. Meanwhile whatwg/html#2791 has been active since 2017.

Developers Still Want This

“PHP was literally created to solve this problem… Imagine if the client could cache the entire header and footer and only need to download the main content.”

— @grepsedawk (whatwg/html#2791)

“DRY HTML authoring without running code server-side or requiring JS. It's actually what I naively expected HTML imports to do at first.”

— @brandondees (74 👍) · GitHub has used <include-fragment> in prod for 10+ years (@josh)

Developers in whatwg/html#2791 repeatedly highlight both the beginner/static-site ergonomics and the caching win of client-side includes. GitHub has used its <include-fragment> custom element in production for over a decade as a client-evaluated <esi:include>.

Benefits

Client-side includes unlock four main benefits: buildless DRY HTML on static hosts without a server or build step; split caching between shared chrome or CDN-cached shells and dynamic user islands; loading async subsections (comments, feeds, recommendations) without delaying the main page; and composing fragments across separate services with native sanitization instead of iframes.

Why It Stalled

  1. Parser-blocking
  2. Layout shift / FOUC
  3. XSS

Client-side includes stalled on three core problems: synchronous fetching created parser-blocking network waterfalls, asynchronous fetching caused FOUC and layout shifts, and injecting fetched HTML carried XSS risk without a native sanitizer.

What Changed?

  1. Parser blocking → <template for>
  2. Layout shift / FOUC → render blocking & buffer (see later...)
  3. XSS → sanitizer

Each of those three blockers now has a platform primitive: out-of-order streaming (<template for>) decouples fetching from parser position, rel="expect" and buffer prevent FOUC and manage layout shifts, and the Sanitizer API makes external markup safe by default.

Proposal: <template for src>

<main> <?start name="feed">Loading...<?end> </main> <!-- Out-of-order stream from an external URL --> <template for="feed" src="feed.html"></template>

Streams from a URL into a location in the DOM

Building directly on out-of-order streaming (<template for="name">), adding a src attribute fetches an external HTML fragment and streams it into the matching <?start>...<?end> marker range.

1. Non-Blocking & Out-of-Order

<!-- Start fetch early --> <main> <?start name="feed"><p class="skeleton">Loading...</p><?end> </main> <template for="feed" src="feed.html"></template> <footer>Renders immediately!</footer>

First mitigation: instead of blocking the HTML parser on a synchronous fetch, <template for src> fetches asynchronously (delaying DOMContentLoaded like <script defer>) so fetches can start early while the parser renders placeholders and <footer> immediately.

2a. Render-blocking instead of parser-blocking

Sync include: Wait header.html Parse <main> → Late Paint Async include: Parse & Paint <main> header.html arrives → Layout Shift! src + expect: Parse <main> + Fetch header.html in parallel → Single Paint (0 CLS) <head><link rel="expect" href="#content" blocking="render"></head> <body> <header><?start name="nav"><?end></header> <template for="nav" src="header.html"></template> <main id="content">Content</main> </body>

Second mitigation (part 1): unlike media subresources such as <img src> (where rel="expect" only waits for the tag to be parsed, not fetched), <template for src> expands into DOM structure—so expecting #content also waits for any client-side includes preceding #content in tree order. The HTML parser continues through <main> without blocking, and header + main paint together.

2b. To stream or not to stream

Default: Streams chunks directly into DOM → Half-rendered UI buffer: Parses into the template → Atomic swap on EOF <!-- Default: streams into DOM --> <template for="feed" src="feed.html"></template> <!-- Buffered: atomic swap on EOF --> <template for="feed" src="feed.html" buffer></template>

Second mitigation (part 2): for async fragments that shouldn't block initial page render, adding buffer parses the response into the template and swaps the placeholder atomically when ready.

How buffer Works in Practice

In practice, a buffered <template> isn't consumed immediately on its start tag: it stays in the DOM while chunks stream into its inert template contents, and once complete, its contents are moved into the target destination and the <template> element is removed.

buffer on inline HTML

<main><?start name="comments">Loading...<?end></main> <template for="comments" buffer> <article class="comment">...</article> <article class="comment">...</article> </template>

Because buffer is a general <template> primitive rather than a fetch-only flag, you can also use <template for="comments" buffer> on the incoming main HTML stream so streamed patches swap in atomically.

buffer Waits for Subresources

<template for="widget" buffer> <link rel="stylesheet" href="widget.css"> <link rel="preload" as="image" href="avatar.jpg"> <div class="widget"><?start name="icon"><?end></div> <template for="icon" src="star.svg"></template> </template>

While buffering into the template, the speculative preload scanner initiates subresource fetches. The atomic swap waits for external stylesheets, <link rel="preload">, and any internal <template for src> includes inside the buffered template to finish.

3. Safe by Default

<!-- Safe by default: sanitized with Sanitizer API --> <template for="promo" src="https://ads.example/banner.html"></template> <!-- Opt-out for trusted code --> <template for="app" src="/trusted-app.html" sanitize="unsafe"></template>

Third mitigation: external includes (<template for src>) are sanitized by default using the browser's built-in Sanitizer API, which strips scripts, inline event handlers, and nested <template for> (preventing slot-hijacking or recursive includes).

In-Place Shorthand: Empty for

<!-- Replaces <template> in place (no <?start>/<?end> needed) --> <template for src="header.html"></template> <template for buffer>...</template>

When you don't need out-of-order targeting, an empty for attribute (<template for src="header.html">, <template for buffer>, <template for sanitize>) acts in place—replacing the <template> element itself without needing <?start>/<?end> markers, and composing cleanly inside <template shadowrootmode="open">.

Observing Patches & Errors (MutationObserver)

observer.observe(main, { childList: true, subtree: true, activeTemplateAttributeFilter: ["for", "src", "data-turn"] }); for (const r of records) { const src = r.activeTemplateAttributes?.get("src"); // r.activeTemplateStatus: "start" | "update" | "end" | "abort" }

Because <template for> is consumed by the parser and never enters the DOM, how does JS observe patch metadata or recover from a failed src fetch / truncated stream? Opting into activeTemplateAttributeFilter in MutationObserver.observe() populates record.activeTemplateAttributes and record.activeTemplateStatus ("start", "update", "end", "abort").

Additional Details

Additional details: <template src> is always CORS; CSP adds fragment-src (falling back to default-src) while script-src still governs sanitize="unsafe"; relative URLs inside fragments resolve against document.baseURI; and external SVGs can be included inline with CSS currentColor styling and built-in sanitization.

Why <template>?

A new <fragment> element is rejected because <template> already has the self-consuming "disappear from the DOM" behavior and is valid everywhere in the HTML parser without foster-parenting.

Putting It All Together

<template <!-- fetch external HTML fragment (CORS) --> src="/widget.html" <!-- stage in template & swap. Some subresources block --> buffer <!-- src sanitized by default; "unsafe" opts out --> sanitize="unsafe" <!-- in-place --> for > <!-- inline HTML can also be buffered --> </template>

Summary of all the new <template> attributes in one place: for (named slot or empty for in-place), src (external CORS fetch), buffer (atomic swap after stylesheets/preloads/nested includes), and sanitize (safe by default with Sanitizer API; sanitize="unsafe" opts out).

Discussion

Try it: Chrome Canary flag · WICG/declarative-partial-updates

Open the floor for feedback on <template src>, buffer, sanitize, and rel="expect" integration.

Quickly highlight sibling developments in WICG/declarative-partial-updates: progressive shells (<?shellboundary>), DOM preservation/merging (preserve attribute for islands inside patched ranges), and parser-aware MutationObserver. Invite everyone to the Seamless Navigation breakout on Wednesday at 9:00 and the WHATWG session.