demo · v148
Streaming vs Sync
Both lanes are fetching the same HTML over a slow simulated connection. The left lane pipes the fetch body into the target via streamHTMLUnsafe(). The right lane awaits the full response then assigns it with setHTMLUnsafe(). Hit Run and watch them race.
About this demo: The "fetch" is a local
ReadableStream we control directly — the slider on the right lets you set the inter-chunk delay so the difference is visible. In a real app, this is what your TTFB-to-fully-loaded curve looks like when you upgrade fetch().then(r => r.text()).then(t => (el.innerHTML = t)) to fetch().then(r => r.body.pipeThrough(new TextDecoderStream()).pipeTo(el.streamHTMLUnsafe())).
streamHTMLUnsafe()
first paint — ms · done — mssetHTMLUnsafe() after full fetch
first paint — ms · done — msthe code
// Streaming lane: pipe the fetch body straight into the element
const sink = target.streamHTMLUnsafe();
const res = await fetch(url);
await res.body
.pipeThrough(new TextDecoderStream())
.pipeTo(sink);
// Sync lane: collect the whole body first, then assign
const res = await fetch(url);
const text = await res.text();
target.setHTMLUnsafe(text);
The streaming version starts painting from the first chunk. The sync version paints once, at the end. The longer the body or the slower the connection, the bigger the gap.
see also
- Renewed HTML insertion & streaming methods — feature index
- Method Cookbook — sibling demo
- WICG explainer: dynamic markup revamped
- v148: Out-of-order streaming — the spec PR that unblocked these methods
implementation reference
Need the exact API surface, compatibility boundaries, errors, lifecycle, and source links? Read the matching gendn reference ↗