Streaming HTML with textStream() in Chrome
JavaScript API · Updated 2026

Streaming HTML with textStream()
in Chrome

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.

Oleg Maximov June 17, 2026 10 min read

Why Streaming HTML Matters

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.

textStream(): The Foundation

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.

Before textStream()

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

With textStream()

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

Streaming DOM Methods

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

Safe Stream Methods

These methods always sanitize the HTML, removing scripts and potentially dangerous elements:

Unsafe Stream Methods

These methods do not sanitize by default, but accept an options object with a sanitizer and/or runScripts property:

Key Distinction

Practical Example: Streaming a Chat Feed

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

Comparison with Non-Streaming Methods

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:

Safe Methods: setHTML and streamHTML

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.

Declarative Partial Updates

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.

How It Works

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.

Running Scripts in Streamed HTML

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.

Browser Support

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.

Browser Support Summary

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.

Practical Use Cases

💬

Live Chat / Comments

Stream incoming messages directly into the DOM as they arrive from the server, without polling or WebSocket message handlers for rendering.

📊

Dashboard Widgets

Load dashboard components progressively — each widget renders as its HTML chunk arrives, improving perceived performance.

📝

Server-Driven Partial Updates

Use declarative partial updates to update specific page sections from the server without client-side routing or state management.

🔌

Streaming WYSIWYG Content

Pipe server-rendered rich text (markdown, ProseMirror, or custom formats) directly into a content area as the server generates it.

Migration Guidance

If you're currently using innerHTML or insertAdjacentHTML to insert HTML from fetch() responses, here's the migration path:

  1. Start with the non-streaming replacements — replace innerHTML with setHTMLUnsafe (or setHTML for sanitized content). These work today and don't require streaming.
  2. Add streaming when appropriate — for long-lived responses (chat, dashboards, partial updates), switch to textStream().pipeTo(element.streamAppendHTML()).
  3. Always use safe methods for untrusted content — user-generated HTML must go through a safe method. The sanitizer is built in and removes XSS vectors automatically.

FAQ

What is textStream() in JavaScript?
textStream() is a new method on Response and Blob objects that returns a stream of text chunks. It's equivalent to piping the byte stream through a TextDecoderStream with UTF-8 decoding, but with a single method call instead of manual piping. This makes streaming HTML from fetch() significantly simpler.
What streaming DOM methods are available in Chrome Canary?
Chrome Canary offers 12 new streaming DOM methods: streamHTML, streamReplaceWithHTML, streamAppendHTML, streamPrependHTML, streamBeforeHTML, streamAfterHTML, plus their Unsafe variants (streamHTMLUnsafe, etc.). Safe methods sanitize input by default; Unsafe methods do not sanitize but can opt into sanitization or script execution via options.
What is the difference between safe and unsafe streaming methods?
Safe methods (streamHTML, streamAppendHTML, etc.) always sanitize the HTML, removing scripts and potentially hazardous elements. Unsafe variants (streamHTMLUnsafe, streamAppendHTMLUnsafe, etc.) do not sanitize by default but can optionally use a sanitizer or enable script execution via {runScripts: true}. For user-generated content, always use safe methods.
How is textStream() different from response.text()?
response.text() returns the entire response as a single string, buffering everything in memory before resolving. textStream() returns a ReadableStream of string chunks, allowing incremental processing — you can start rendering content before the full response has arrived. This reduces time-to-first-byte and improves perceived performance.
What are declarative partial updates with streaming HTML?
Declarative partial updates let you define <template for="name"> elements in fetched HTML and use <?marker name="name"> processing instructions in the initial page markup. When streamed to document.body, each template's content automatically replaces its matching marker by name. This enables server-driven partial page updates without JavaScript selectors or client-side routing.
What browser supports textStream() and streaming DOM methods?
As of June 2026, both textStream() and the streaming DOM methods are supported exclusively in Chrome Canary. They are bleeding-edge features not yet available in Chrome Stable, Firefox, Safari, or any other browser. Edge will follow the Chrome release schedule once the feature stabilizes.
Can I run scripts in streamed HTML?
Yes, but only with unsafe methods and explicit opt-in. Use streamAppendHTMLUnsafe({runScripts: true}) or similar. Safe methods never execute scripts, and unsafe methods require the runScripts: true option — this is a deliberate security design that prevents accidental XSS even in the unsafe code path.

Conclusion

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.

Contact

Need help with modern web APIs?

Building an application that uses streaming, real-time updates, or progressive loading? I can help architect and implement it. Free initial consultation.