Hoi hoi!
I'm @nyaomaru, a frontend engineer who was cheering for the World Cup during tropical nights in the Netherlands while nearly melting from heat exhaustion. 🫠⚽ (But yesterday was comfortable 😺)
How do you usually build frontend applications?
Do you build SPAs with React / Vue / Svelte?
Do you use SSR / SSG / ISR frameworks like Next / Remix / Nuxt / Astro?
There are many ways to build frontend applications, but today it is very common to update the DOM on the client side using JavaScript.
Even when using UI libraries and frameworks like React / Vue / Svelte, or SSR frameworks like Next / Remix / Nuxt / Astro, there are still many cases where part of the screen is eventually updated by client-side JavaScript.
But is that really always the best answer?
Are there cases where we are forcing JavaScript on the client to handle work that could naturally be handled on the server?
In response to that kind of question, the Chrome team introduced an experimental API called DPU, or Declarative Partial Updates. 🚀
In this article, I want to explore what Declarative Partial Updates are, what problems they try to solve, and why they might matter.
I will focus especially on the out-of-order streaming mechanism that can replace parts of HTML later without writing client-side JavaScript for the replacement itself.
Let's dive in!
🧩 What are Declarative Partial Updates?
Declarative Partial Updates are, roughly speaking
A mechanism where you declare part of your HTML as “this area will be replaced later”, and then update only that part with HTML that arrives later.
For example, imagine you want to return the overall page HTML immediately, but some data is slower to fetch.
- The header is instant
- The layout is fast
- The user profile is also pretty fast
- But only the recommendations API is slow
In that case, we often show a loading state on the client, fetch data, and update the DOM with JavaScript. In React, you might reach for something like <Suspense>.
With DPU, the server can first return HTML like this.
<section>
<h2>Recommendations</h2>
<?start name="recommendations">
<p>Loading recommendations...</p>
<?end>
</section>
The area surrounded by <?start name="recommendations"> and <?end> is a region that can be replaced later.
Then, when the recommendations HTML is ready on the server, it can stream HTML like this later.
<template for="recommendations">
<ul>
<li>Advanced CSS Layouts</li>
<li>Modern HTML APIs</li>
<li>Web Performance Basics</li>
</ul>
</template>
At that point, the browser finds the region named recommendations and replaces the loading UI with the contents of this <template>.
Wait, does that feel a bit weird?
Exactly. The wild part is that up to this point, we did not write any JavaScript for the replacement, and no framework is involved.
In other words, you no longer need to write client-side JavaScript like this just to replace that area:
const element = document.querySelector("#recommendations");
element.innerHTML = html;
You stream HTML, and the browser understands where that HTML should go.
That is the interesting part of Declarative Partial Updates.
DPU also has <?marker>, not only <?start> / <?end>.
<?marker> is basically a marker that says, “insert later HTML here.”
<div>
<?marker name="placeholder">
</div>
<template for="placeholder">
<p>Here is streamed content!</p>
</template>
In this case, the browser finds the marker named placeholder and inserts the contents of the later <template for="placeholder"> at that position.
On the other hand, the <?start> / <?end> pair used mainly in this article is for showing placeholder content, such as a loading UI, and replacing that entire range later.
So, roughly
-
<?marker>means “insert here” -
<?start>/<?end>means “replace this range”
That mental model should be enough to start.
Note
<?marker>can also be used by leaving the same marker inside streamed content, which lets you append list items one by one. That is pretty interesting too.In the first half of this article, I focus mainly on replacing loading UI, so I mostly use
<?start>/<?end>.In the later sample, I also try an example that uses
<?marker>to append asset links sequentially.
🤔 Why do we need Declarative Partial Updates?
At this point, you might think
Wait, frameworks like React or Astro can already do this, right?
Yes. They can.
With React / Vue / Svelte / Next / Remix / Nuxt / Astro, you can show loading states, receive API results, and update parts of the screen. Many of us do this every day.
But wait a second. A lot of that is implemented by client-side JavaScript updating the DOM, right?
A common flow looks like this:
- The server returns HTML
- The browser displays the HTML
- Client-side JavaScript is loaded
- JavaScript calls an API
- The API result is put into state
- The framework updates the DOM
Of course, this is an extremely common frontend architecture today.
But if part of the screen is “something the server can already produce as HTML”, do we really need to make client-side JavaScript do that work every time?
For example, the recommendations HTML can be generated on the server like this:
<ul>
<li>Advanced CSS Layouts</li>
<li>Modern HTML APIs</li>
<li>Web Performance Basics</li>
</ul>
If all we want to do is send that to the browser, it can feel a bit indirect to do this.
fetch("/api/recommendations")
.then((res) => res.json())
.then((items) => {
setRecommendations(items);
});
You fetch JSON, convert it to HTML on the client, update state, and then update the DOM.
That is convenient, but it is also a bit roundabout.
What makes DPU interesting is that it points in another direction
If the server can produce HTML, why not stream that HTML directly and let the browser insert it into the right place?
So DPU is not a replacement for React / Vue / Svelte. It is more like a lower-level browser primitive.
It may allow the browser to understand, as part of HTML itself, some of the partial update work that frameworks currently handle.
If that happens, the boundary between client-side responsibility and server-side responsibility becomes even more important.
- What can be done with HTML?
- What should be done with JavaScript?
By redesigning these boundaries and declaring things in a way that fits the browser, we may be able to unlock browser capabilities that we are not fully using yet.
But we should not misunderstand the client still has responsibilities.
This discussion is not about replacing state management, and interactive features still need JavaScript.
For example, forms, modals, charts, and similar UI should still be managed on the client side, while static text or server-generated HTML based on slower API responses may be a better fit for HTML streaming.
The responsibility split becomes more complex, but the UX may improve a lot. This is definitely something worth watching.
🏠 This may expand what SPAs can do
DPU is not about rejecting SPAs.
Personally, I see it as something that may expand the possibilities of SPAs.
Until now, SPAs have often followed this mental model.
Fetch data as JSON
↓
Put it into client-side state
↓
Re-render components
↓
Update the DOM
But if DPU and the new HTML insertion / streaming APIs become available, another option appears.
The server returns HTML
↓
HTML is streamed
↓
The browser inserts it into the existing DOM
That means an SPA could potentially choose different update strategies for different parts of the UI:
- Fetch JSON and render on the client
- Generate HTML on the server and insert it directly
- Return only the shell initially, then stream slower parts later
- Perform page-transition-like updates with HTML partials
That is really interesting.
SPAs often carry the image of “doing everything with client-side JavaScript”, but they may become more hybrid in the future. The meaning of the word SPA itself may change.
- State that belongs on the client stays on the client
- HTML that can be produced on the server is produced on the server
And the browser does not merely display that HTML. It also understands where and how to insert it.
If this direction continues, frontend architecture may become even more flexible and deeper than it is today.
🧪 APIs for streaming HTML from JavaScript are also coming
The interesting part of DPU is not only out-of-order streaming with <template for="...">.
The Chrome team is also proposing a new set of APIs for inserting and streaming HTML from JavaScript.
Even today, there are several ways to dynamically insert HTML.
element.innerHTML = html;
element.outerHTML = html;
element.insertAdjacentHTML("beforeend", html);
But they all have slightly different behavior.
For example:
- Does it overwrite existing content?
- Does it append after the existing content?
- Are
<script>tags executed? - What happens with sanitization?
- How does it interact with Trusted Types?
There are a lot of subtle differences.
Honestly, I do not think many developers can confidently explain all of those differences from memory. I definitely cannot.
The new APIs try to make HTML insertion easier to understand with more consistent names.
For example, static HTML insertion looks like this.
const newHTML = "<p>This is a new paragraph</p>";
const contentElement = document.querySelector("#content-to-update");
contentElement.setHTML(newHTML);
This is not an API that appends to the end, like insertAdjacentHTML("beforeend", html). It replaces the existing contents of contentElement with newHTML.
A useful way to understand the difference is
-
setHTML()is the safer API that runs through a sanitizer -
setHTMLUnsafe()disables the sanitizer by default and lets you handle HTML more directly
Then comes the really interesting part of streaming versions.
const contentElement = document.querySelector("#content-to-update");
const response = await fetch("/api/content.html");
response.body
.pipeThrough(new TextDecoderStream())
.pipeTo(contentElement.streamHTMLUnsafe());
This is pretty wild.
It means we may be able to take HTML returned by fetch() and stream it directly into an element without waiting for the entire response to finish downloading.
SPAs have not benefited much from HTML streaming after the initial document load.
The initial document can be streamed from the server, but later screen updates often become JSON fetch + client render.
With APIs like this, SPA updates may also be able to benefit from HTML streaming.
That is why I think this may expand the possibilities of SPAs.
⚠️ “unsafe” does not mean “never use it”
When you see a name like streamHTMLUnsafe(), you might think
Unsafe!? That sounds terrifying.
I thought that too at first.
But this unsafe does not mean “you must never use it.” It is closer to a warning that the sanitizer is disabled by default, so you need to think carefully about whether the input is trusted.
For example, it would be dangerous to stream user-generated HTML directly into the DOM.
But if the HTML is generated by your own trusted server, there may be valid use cases.
Of course, before using this in production, you need to think carefully about sanitizers, Trusted Types, script execution, browser support, and other security concerns.
➕ What happens when DPU and the streaming APIs are combined?
This is the most interesting part.
DPU’s <template for="..."> and the new HTML streaming APIs are separate APIs.
But combining them makes the story even deeper.
For example, imagine placing placeholders in an SPA screen.
<main>
<?start name="page-title">
<h1>Loading...</h1>
<?end>
<?start name="page-content">
<p>Loading content...</p>
<?end>
</main>
Then, during a page update, the server streams HTML.
<template for="page-title">
<h1>Dashboard</h1>
</template>
<template for="page-content">
<section>
<p>This is the dashboard content.</p>
</section>
</template>
Instead of manually targeting the DOM with JavaScript.
document.querySelector("#page-title").innerHTML = titleHTML;
document.querySelector("#page-content").innerHTML = contentHTML;
the browser may be able to understand where page-title and page-content should be applied.
In other words, even in SPA-style page transitions, something like this may become possible.
Fetch a new page's HTML partial
↓
Stream the HTML
↓
Update only the necessary places via template for
Part of the work that frameworks currently handle with Virtual DOM, compilers, or fine-grained reactivity may be shifted to browser-native HTML streaming / partial updates.
That makes DPU feel like more than just a small HTML API. It feels like something that could expand the design space of frontend architecture.
Of course, this is still experimental. This is not a “let's all use this in production right now” kind of API.
But the old assumption
An SPA fetches JSON and renders on the client
may start to change.
HTML may evolve from “a static document loaded once at the beginning” into “a UI unit that can be streamed, replaced, and updated.”
That is pretty exciting, right?
🚀 Trying it with Node.js DPU samples
So far, we have talked about the concept. I also built a small Node.js sample app to try this out.
nyaomaru
/
declarative-partial-updates-sample
Sample repository for DPU (Declarative Partial Updates)
Declarative Partial Updates sample
A small Node.js sample app for trying Chrome's experimental Declarative Partial Updates (DPU) feature.
Images under public/ are served through the Node.js stream handler at /public/...
then inserted from later DPU or HTML streaming chunks.
Styles live in public/styles.css. Shared timing and image constants live in
features/shared.mjs, while public asset metadata and GitHub links live in
features/public-assets.mjs so the sample behavior is easier to inspect and
adjust.
The server is split by feature:
-
features/home/: DPU document streaming sample HTML and stream chunks -
features/hybrid-shell/: hybrid shell HTML and route partial HTML -
features/set-apis/: static and streaming HTML insertion sample HTML -
features/public-assets.mjs: public asset metadata and stream handler -
features/layout/: shared document layout HTML -
features/layout.mjs: layout template renderer -
features/shared.mjs: common helpers and demo timing constants -
public/html-stream.js: client-side static / streaming HTML insertion helper
Run
npm start
Open:
Note
This is an experimental feature, so you need Chrome Canary (Chrome v148 or later) or a Chrome build that includes this feature, with the experimental flag enabled.
Open the following URL in Chrome’s address bar:
chrome://flags/#enable-experimental-web-platform-featuresEnable it, click “Relaunch”, and then run the sample. This article assumes a supported build with this flag enabled.
The demo app has roughly three pages:
/ ... Document streaming
/hybrid-shell ... Hybrid shell + streamed HTML route updates
/set-apis ... Static / Streaming HTML APIs
The first page, /, demonstrates DPU out-of-order streaming.
The server first returns an HTML shell, and slower server-rendered fragments are streamed later as <template for="...">.
For example, the initial HTML contains a placeholder.
<article class="panel">
<h2>Recommendations</h2>
<?start name="recommendations">
<p class="loading">Waiting for recommendations...</p>
<?end>
</article>
At this point, the browser shows the loading UI.
Then, once the recommendations HTML is ready on the server, it streams a chunk.
<template for="recommendations">
<ul class="list">
<li>Advanced CSS Layouts</li>
<li>Modern HTML APIs</li>
<li>Web Performance Basics</li>
</ul>
</template>
The browser finds the DPU range named recommendations and replaces the loading UI with the list.
On this page, I stream multiple named regions at different timings, not only recommendations:
- hero image
- metric
- asset strip
- team activity
- streamed code block
The asset strip also uses <?marker>. The initial HTML has a marker.
<section class="panel asset-panel">
<h2>Streamed public assets</h2>
<div class="asset-grid">
<?marker name="asset-list">
</div>
</section>
Then the server streams chunks one by one whenever each asset link becomes ready.
<template for="asset-list">
<!-- streamed asset link -->
<?marker name="asset-list">
</template>
By leaving the same marker again inside the streamed content, the next asset link can continue to be appended in the same place.
So HTML does not have to wait until everything is complete from top to bottom. Fragments can be streamed in later as soon as they are ready.
That is the really interesting part of DPU.
Note
<template for>has several subtle rules, including constraints related to parent elements.This demo is a sample for exploring the direction of the API, but if you want to use it in production someday, you should carefully check the applicable range of
<template for>, DOM movement during streaming, Trusted Types, sanitizers, and related details.
🌓 Trying a hybrid shell style architecture
Next, /hybrid-shell demonstrates streaming HTML route updates inside a hybrid shell style architecture.
This is not a “pure SPA where everything is rendered on the client”. It is closer to an architecture where the whole document is not reloaded, the URL changes through the History API, and only the route content inside the shell is updated.
The goal is
Keep client-side state alive, while replacing only the route content with server-generated HTML.
For example, this page has a counter:
<div>
Counter: <strong id="counter">0</strong>
<button id="increment" class="primary" type="button">Increment</button>
</div>
This counter is managed by client-side JavaScript.
document.querySelector("#increment").addEventListener("click", () => {
counter.textContent = String(Number(counter.textContent) + 1);
});
So this is clearly a client-side responsibility.
On the other hand, route content is fetched from the server as HTML.
const loadRoute = async (route, { push = false } = {}) => {
setActiveRoute(route);
if (push) {
const nextUrl =
route === "dashboard"
? "/hybrid-shell/dashboard"
: `/hybrid-shell/${route}`;
window.history.pushState({ route }, "", nextUrl);
}
const response = await fetch(`/partials/hybrid-shell/${route}`);
view.replaceChildren();
if ("streamHTMLUnsafe" in Element.prototype && response.body) {
await response.body
.pipeThrough(new TextDecoderStream())
.pipeTo(view.streamHTMLUnsafe());
return;
}
const template = document.createElement("template");
template.innerHTML = await response.text();
view.replaceChildren(template.content.cloneNode(true));
};
What this does is not “fetch JSON and render components on the client.” Instead, it streams HTML generated by the server directly into the view.
The counter state remains on the client.
That means we can split responsibilities:
- Interactive state such as the counter belongs to the client
- Server-generated route body can be streamed as HTML
This feels close to the thinking behind island architecture in frameworks like Astro / Fresh.
Instead of making the client do everything, we separate HTML that can be returned statically or from the server from interactive client state.
Personally, I find this very interesting.
At the very least, a shell with SPA-like navigation does not have to mean “everything is rendered by client-side JavaScript.” It may also expand into a hybrid shape that combines client state with streamed HTML.
Note
This
/hybrid-shellsample is not a demo that replaces a range inside the shell using DPU’s<?start>/<template for>.The Chrome team article also introduces the possibility of SPA-style page loads by combining
streamHTMLUnsafe()and<template for>.However, in my local tests, I could not confirm that streaming
<template for="...">intostreamHTMLUnsafe()in this shell architecture updates an existing DPU range.The SPA-style page load described in the official article seems to point toward an outline page with processing instructions, where the new page’s
<template for>elements are streamed toward the end of the HTML and slotted into those instructions.So in this
/hybrid-shellsample, I do not handle DPU range replacement. Instead, I use the HTML streaming insertion API to stream server-generated route partials directly into#hybrid-shell-view.
🎁 Trying Static / Streaming HTML APIs
Finally, /set-apis tries the new HTML insertion / streaming APIs separately from DPU’s out-of-order streaming.
This page first checks whether the browser has the APIs.
support.innerHTML = `
<ul class="list">
<li>setHTMLUnsafe: ${"setHTMLUnsafe" in Element.prototype}</li>
<li>streamHTMLUnsafe: ${"streamHTMLUnsafe" in Element.prototype}</li>
</ul>
`;
For static insertion, if setHTMLUnsafe() is available, the demo uses it. Otherwise, it falls back to <template> and replaceChildren().
if ("setHTMLUnsafe" in Element.prototype) {
staticTarget.setHTMLUnsafe(markup);
return;
}
const template = document.createElement("template");
template.innerHTML = markup;
staticTarget.replaceChildren(template.content.cloneNode(true));
For streaming insertion, it pipes the fetched HTML response directly into streamHTMLUnsafe():
const response = await fetch("/partials/set-apis/stream");
streamTarget.replaceChildren();
if ("streamHTMLUnsafe" in Element.prototype && response.body) {
await response.body
.pipeThrough(new TextDecoderStream())
.pipeTo(streamTarget.streamHTMLUnsafe());
return;
}
If this works, we can stream chunks into an existing element as they arrive, instead of waiting until the entire HTML response has downloaded and inserting it all at once.
Traditionally, SPAs could benefit from HTML streaming for the initial document, but subsequent screen updates often became JSON fetch + client render.
With APIs like this, SPAs may be able to benefit from HTML streaming even for later updates.
That is the exciting part of DPU and the new HTML streaming APIs.
The static insertion partial is returned from /partials/set-apis/static, and the streaming insertion partial is returned from /partials/set-apis/stream.
🎯 Summary
In this article, I looked at Declarative Partial Updates introduced by the Chrome team.
Declarative Partial Updates are, roughly
A mechanism where you declare part of your HTML as “this area will be replaced later”, and then update only that part with HTML that arrives later.
Of course, DPU does not replace React / Vue / Svelte / Next / Remix / Nuxt / Astro.
Interactive UI, client-side state management, forms, modals, charts, drag and drop, and similar features still need JavaScript.
But on the other hand, there may be more cases where we do not need to handle everything with client-side JavaScript.
- HTML that can be generated on the server should be generated on the server
- State that should be managed on the client should be managed on the client
And the browser may come to understand not only how to display HTML, but also where streamed HTML should be inserted.
If this responsibility split progresses, frontend architecture may become more hybrid.
Especially for SPAs, there may be another path beyond
Fetch JSON and render on the client
Instead, we may also be able to
Fetch / stream HTML partials and update via browser-native behavior
I do not see this as the end of SPAs. I see it as something that may expand what SPAs can do.
This is still experimental, so it is not an API to use in production right away.
But the direction where HTML changes from “a document loaded once at the beginning” into “a UI unit that can be streamed, replaced, and updated” feels very important for the future of frontend development.
DPU is definitely something to keep an eye on. 😺
References
https://developer.chrome.com/blog/declarative-partial-updates?hl=en
https://github.com/WICG/declarative-partial-updates
https://github.com/GoogleChromeLabs/template-for-polyfill
https://github.com/GoogleChromeLabs/html-setters-polyfill
https://github.com/nyaomaru/declarative-partial-updates-sample



Top comments (0)