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. Buffered Includes (buffer)

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.

buffer on the Main Stream

<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 (sanitize)

<!-- 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).

sanitize on the Main Stream

<template for="comments" sanitize> <article>Hello! <script> evil() </script></article> </template>

Just like buffer, sanitize works without src too. While inline <template for> is unsanitized by default because it's part of the main document stream, adding sanitize lets server-rendered pages declaratively sanitize untrusted regions directly in the HTML parser.

In-Place Shorthand: Empty for

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

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.

Discussion

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

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

Also in Declarative Partial Updates (DPU)

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.