Chrome Canary introduces textStream() and a set of streaming DOM methods
that change how we pipe HTML from fetch() into the page. Here's what they do,
how they compare to existing APIs, and when to use them.
Streaming is one of the most impactful performance patterns available on the web. Instead of waiting for an entire resource to download before rendering, streaming lets the browser process data incrementally — painting content as it arrives. The streaming DOM methods in Chrome Canary bring this capability directly into the DOM API layer.
Until now, streaming HTML into a page required manual orchestration: parsing the byte stream
from response.body, piping it through a TextDecoderStream, and
manually appending chunks to the DOM. The new APIs remove most of this boilerplate.
The textStream() method is available on both Response and
Blob objects. It returns a ReadableStream of string chunks,
decoded from the byte stream as UTF-8 text.
To get a stream of text from a fetch() response, you had to pipe through a decoder manually:
const response = await fetch('/partial.html');
const decoder = new TextDecoderStream();
const textStream = response.body.pipeThrough(decoder);
const reader = textStream.getReader();
// ... manually read chunks and insert into DOM
The entire piping step collapses into a single method call:
const response = await fetch('/partial.html');
const textStream = response.textStream();
// textStream is a ReadableStream of string chunks
// Ready to pipe directly into streaming DOM methods
textStream() is equivalent to piping the body through a UTF-8
TextDecoderStream, but with zero configuration. It also works on
Blob objects, making it a general-purpose text streaming primitive:
const blob = new Blob(['<h1>Hello</h1><p>World</p>']);
const stream = blob.textStream();
// Use the same streaming pipe pattern
Chrome Canary introduces 12 new methods on Element that accept a text stream
and render it into the DOM incrementally. They come in two categories: safe
(always sanitize) and unsafe (no sanitization by default).
These methods always sanitize the HTML, removing scripts and potentially dangerous elements:
streamHTML() — replaces element content with streamed HTMLstreamReplaceWithHTML() — replaces the element itselfstreamAppendHTML() — appends streamed HTML as the last childstreamPrependHTML() — inserts streamed HTML as the first childstreamBeforeHTML() — inserts streamed HTML before the elementstreamAfterHTML() — inserts streamed HTML after the elementThese methods do not sanitize by default, but accept an options object with a
sanitizer and/or runScripts property:
streamHTMLUnsafe()streamReplaceWithHTMLUnsafe()streamAppendHTMLUnsafe()streamPrependHTMLUnsafe()streamBeforeHTMLUnsafe()streamAfterHTMLUnsafe(){runScripts: true}. Use for trusted content from your own server.Here's how you'd use the streaming DOM for a live chat or comment feed:
const feed = document.getElementById('chat-feed');
async function streamMessages() {
const response = await fetch('/api/chat/stream');
await response.textStream()
.pipeTo(feed.streamAppendHTML());
}
// Each chunk of HTML from the server renders
// immediately as a new message in the feed
Alongside the streaming methods, Chrome also introduces non-streaming counterparts to the legacy DOM insertion APIs. Here's the complete picture:
| Legacy Method | New (Non-Streaming) | Stream Safe | Stream Unsafe |
|---|---|---|---|
innerHTML legacy |
setHTMLUnsafe new |
streamHTML safe |
streamHTMLUnsafe unsafe |
outerHTML legacy |
replaceWithHTMLUnsafe new |
streamReplaceWithHTML safe |
streamReplaceWithHTMLUnsafe unsafe |
insertAdjacentHTML(beforeend) legacy |
appendHTMLUnsafe new |
streamAppendHTML safe |
streamAppendHTMLUnsafe unsafe |
insertAdjacentHTML(afterbegin) legacy |
prependHTMLUnsafe new |
streamPrependHTML safe |
streamPrependHTMLUnsafe unsafe |
insertAdjacentHTML(beforebegin) legacy |
beforeHTMLUnsafe new |
streamBeforeHTML safe |
streamBeforeHTMLUnsafe unsafe |
insertAdjacentHTML(afterend) legacy |
afterHTMLUnsafe new |
streamAfterHTML safe |
streamAfterHTMLUnsafe unsafe |
Despite the naming, the new new non-streaming methods
with Unsafe are your direct replacements for legacy methods. They differ from the
old APIs in two important ways:
<template shadowrootmode> elements{runScripts: true} to execute scripts in the inserted HTML
The safe non-streaming method setHTML and its streaming counterpart
streamHTML use the Sanitizer API
under the hood. This is the same mechanism used by the Sanitizer class, which
strips scripts, event handlers, and other potentially hazardous content while preserving safe markup.
One of the most interesting use cases for the streaming DOM is declarative partial
updates. This pattern combines <template> elements with server-driven
processing instructions to update specific page sections without any JavaScript selectors.
The server streams HTML that contains <template for="name"> elements.
The initial page markup uses <?marker name="name"> processing instructions:
<!-- Initial page markup -->
<main>
<?marker name="main-content">
</main>
<aside>
<?marker name="sidebar">
</aside>
The server responds with template definitions:
<template for="main-content">
<article>
<h1>Streaming HTML Guide</h1>
<p>Content loads progressively...</p>
</article>
</template>
<template for="sidebar">
<div class="widget">
<h3>Related Topics</h3>
<ul>...</ul>
</div>
</template>
The streaming pipe connects them:
const response = await fetch('/page-with-partials.html');
await response.textStream()
.pipeTo(document.body.streamAppendHTMLUnsafe());
When using safe methods, note that <template> elements are removed by
default during sanitization. To allow them, pass an empty sanitizer config:
await response.textStream()
.pipeTo(document.body.streamAppendHTML({sanitizer: {}}));
The matching for and name attributes determine which template
replaces which marker. No querySelector, no innerHTML assignments —
the framework handles the mapping automatically.
If the HTML you're streaming contains <script> tags that need to execute,
you must use an unsafe method with the runScripts option:
const response = await fetch('/dynamic-widget.html');
await response.textStream()
.pipeTo(
document.body.streamAppendHTMLUnsafe({runScripts: true})
);
This is a deliberate design choice — safe methods never execute scripts, and unsafe methods require explicit opt-in. This prevents XSS vulnerabilities even when using the unsafe family.
As of June 2026, both textStream() and the streaming DOM methods are available
in Chrome Canary only. They have not yet shipped in Chrome Stable, and no
signals from Firefox or Safari about implementation.
For production use, stick with the non-streaming equivalents or polyfill-based approaches. The streaming APIs are ideal for experimentation, progressive enhancement, and preparing for future browser adoption.
Stream incoming messages directly into the DOM as they arrive from the server, without polling or WebSocket message handlers for rendering.
Load dashboard components progressively — each widget renders as its HTML chunk arrives, improving perceived performance.
Use declarative partial updates to update specific page sections from the server without client-side routing or state management.
Pipe server-rendered rich text (markdown, ProseMirror, or custom formats) directly into a content area as the server generates it.
If you're currently using innerHTML or insertAdjacentHTML to insert
HTML from fetch() responses, here's the migration path:
innerHTML with setHTMLUnsafe (or setHTML for sanitized content). These work today and don't require streaming.textStream().pipeTo(element.streamAppendHTML()).
The textStream() API and streaming DOM methods represent a meaningful step forward
for DOM manipulation. By combining the streaming primitives from the Streams API with direct DOM
insertion, Chrome is making it dramatically easier to build pages that load progressively and
respond in real time.
For now, these are bleeding-edge APIs available only in Chrome Canary. But the patterns they introduce — declarative partial updates, safe-by-default sanitization, and streaming as a first-class concept — will likely influence the future of web development across all browsers.
If you're building a web application that needs real-time updates, progressive loading, or server-driven partial rendering, experimenting with these APIs today will prepare you for the direction the platform is heading.
Building an application that uses streaming, real-time updates, or progressive loading? I can help architect and implement it. Free initial consultation.