Skip to content

feat: template-for based streaming - #474

Open
MaxwellCohen wants to merge 7 commits into
preactjs:mainfrom
MaxwellCohen:dpu-template-for-streaming
Open

feat: template-for based streaming#474
MaxwellCohen wants to merge 7 commits into
preactjs:mainfrom
MaxwellCohen:dpu-template-for-streaming

Conversation

@MaxwellCohen

@MaxwellCohen MaxwellCohen commented Aug 27, 2026

Copy link
Copy Markdown

Update the template streaming markers to enable <template for > streaming with a MutationObserver-based polyfill.

This is a slightly optimized approach for template-for-based streaming compared to #468 (this is a very helpful pr)
The key changes are:

  • Using the existing streaming boundaries (<!--$s:id-->…<!--/$s:id-->) for fallback detection; this saves a few bytes in the inline script and removes the need for name=id in <?end > tag
  • Moved to more of a progressive enhancement vs. polyfill approach
  • General refactoring to save inline script size
  • Added tests for the inline script

Updated comparison with current client.js vs previous preact-island.

Example: id=5, fallback="loading...", content=<p>it works</p>, no nonce.

Previous (island) Current (DPU) Δ
Markers 23 46 +23
Fallback 10 10 0
Subtree wrap 54 29 −25
Per-boundary 87 85 −2
Init script 606 606 0
Hidden <div> 18 0 −18
Once total 624 606 −18

Total wire bytes

Boundaries Prev Current Δ
1 711 691 −20
2 798 776 −22
5 1059 1031 −28
10 1494 1456 −38

The summary was generated with the help of LLMS

You can test it out at:
I have created this demo app: https://pract-dpu.vercel.app/ repo: https://github.com/MaxwellCohen/pract-dpu
Here is a build of this code in CodeSandbox: https://ci.codesandbox.io/status/MaxwellCohen/preact-render-to-string/pr/2/builds/691245

@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6a02913

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
preact-render-to-string Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@MaxwellCohen
MaxwellCohen force-pushed the dpu-template-for-streaming branch from 556ddc6 to ee6ebaf Compare August 30, 2026 05:07
Comment thread src/lib/client.js Outdated
// p = e.previousSibling;
// (function initPreactPatch(d) {
// if ("htmlFor" in HTMLTemplateElement.prototype) return;
// new MutationObserver(function (records) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One issue here is that this observer will be attached as soon as a <template for is present but during streaming the DOM can interleave creation with a micro-task. If that happens the current logic would make it so that we append an empty template content and due to us removing it it would never be completed. In the polyfill it's also deferred until it's ready https://github.com/GoogleChromeLabs/template-for-polyfill/blob/main/src/template-for-polyfill.ts#L22-L26

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review. I will work on removing issues later today. I am also seeing edge-case issues with SVG and MathML. That needs to be figured out.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated this to cover this race condition

Here is the updated size when added extra checks
Updated comparison with current client.js vs previous preact-island.

Previous (island) Current (DPU) Δ
Markers 23 46 +23
Fallback 10 10 0
Subtree wrap 54 29 −25
Per-boundary 87 85 −2
Init script 606 584 −22
Hidden <div> 18 0 −18
Once total 624 584 −40

Total wire bytes

Boundaries Prev Current Δ
1 711 669 −42
2 798 754 −44
5 1059 1009 −50
10 1494 1434 −60

Comment thread package-lock.json

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're missing some yaml thing according to CI, just wanted to flag it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not run npm i on this branch causing this issue

@JoviDeCroock JoviDeCroock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good so far, we do need to have a harness to test this (preferrably with v11) so we know that there are no hydration missmatches when it's supported and unsupported, that we support Fragments enclosing > 1 DOM element, ....

Also will need a changeset indicating a major version

@MaxwellCohen
MaxwellCohen force-pushed the dpu-template-for-streaming branch 2 times, most recently from 282c63f to 5be1480 Compare September 2, 2026 04:24
Comment thread src/lib/client.js
Comment on lines +27 to +30
// // re-parse SVG and MathML elements so they will be rendered correctly
// for ( node of d[qsa]("svg *,math *")) {
// if (node.tagName < "a" && (node = node.closest("svg,math"))) {
// node.innerHTML += "";

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines are for handling the re-parsing of replacements inside SVGs and MathML because the template for replacement treats all elements as HTML, not SVG or MathML, so the replacement will not work

Here is a simple example page of this issue: https://fast-page-ten.vercel.app/streaming.html

Comment thread src/lib/client.js

// let mo = new MutationObserver(initPreactPatch);
// mo.observe(d, { childList: 1, subtree: 1 });
// d.addEventListener("DOMContentLoaded", initPreactPatch);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the DOM is loaded, we run patch 1 one last time and disconnect the mutation observer, since once the page has finished streaming (loading), we will not need to make any further replacements. Since I have extra patching checks for SVG and MathML. This is more of a progressive enhancement than a polyfill.

@JoviDeCroock JoviDeCroock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great and works in all my tests

@JoviDeCroock

Copy link
Copy Markdown
Member

I found an issue when the client resolves a boundary that is still to be streamed in, we duplicate dom

@MaxwellCohen

Copy link
Copy Markdown
Author

I found an issue when the client resolves a boundary that is still to be streamed in, we duplicate the DOM

This is not good. I have created a fix based on your description. If I can get an example, it would make verification easier (I have a unit test but have not seen it for myself in the browser).

@JoviDeCroock

Copy link
Copy Markdown
Member

@MaxwellCohen I'll ask an agent to PR my tests (and a few fixes) to your fork, I can't currently make it to a pc 😅

@MaxwellCohen
MaxwellCohen force-pushed the dpu-template-for-streaming branch from 90cb47d to 93dec46 Compare September 7, 2026 11:56
@MaxwellCohen

Copy link
Copy Markdown
Author

I have updated this branch and created #478 to better handle the streaming edge cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants