Guide · Frontend
Out of Order HTML Streaming: Show the Page Shell First, Slow Parts Later
19 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Out of order HTML streaming lets a server send the page shell, the parts that are ready, at once. Slow sections follow later in the same response, in whatever order they finish.
- Each late section travels inside a template element, an HTML element whose content the browser keeps hidden. Its for attribute names a placeholder, and the browser moves the content there without JavaScript.
- A placeholder is a processing instruction: a small marker written as a less-than sign, a question mark and a word such as start or marker. A start and end pair can hold loading text.
- As of 19 October 2026, Chrome and Edge support it from version 150. Firefox and Safari do not, so those visitors need fallback content or a polyfill, a script that adds a missing feature.
- Beginners can stop after the browser support section. Engineers then get a runnable Python server with a timing check and rules tested in Chromium 153, the open-source core of Chrome and Edge. They also see why the polyfill and a default nginx proxy, a server in front of your app, each held every slot until the end in our tests.
What goes wrong when one slow service holds up a whole web page?
Visitors stare at a blank screen until the slowest part of the page is ready. Out of order HTML streaming lets the ready parts show first, while slow parts arrive later in the same response.
Picture a common setup. A travel site sells houseboat packages in Kerala, and its busiest page shows one package. The setup is made up, but the problem is real.
The title, photos and itinerary come from a cache in about 150 milliseconds (made up). The reviews take 1.6 seconds. Today's price takes 2.8 seconds, because the pricing service checks which boats are free.
The page server waits for all three before it sends any HTML. So every visitor sees a white screen for nearly three seconds, even though most of the page was ready almost at once.
The usual fix is the naive one. Send the page early, then fetch the price and reviews with JavaScript after it loads. It works, but the slow calls only start once the script runs, so the price arrives even later.
What is out of order HTML streaming?
Out of order HTML streaming means the server sends one HTML response, but not in page order. The shell comes first with named gaps, and each slow section arrives later to fill its gap.
Streaming itself is old. Browsers have long shown HTML as it arrives over the network. The catch was order: HTML had to arrive top to bottom, so a slow price held back everything below it.
Think of a thali at a busy restaurant. The waiter first puts down the plate with empty, labelled katoris (small bowls). Each dish goes into its katori when the kitchen finishes it, in any order.
The plate is the shell. Each labelled katori is a named placeholder. Each dish is a template that carries the label of its katori. Where the picture breaks: a waiter can fill any katori on the table. A template can only fill placeholders inside its own parent element, unless it sits directly inside body.
One response: shell first, then patches
Drag to exploreNotice that the connection stays open the whole time. Reviews land before the price because they finish first, not because they come first on the page. First paint is the moment anything appears on screen.
Diagram connections:
- Browser asks for the page to Shell with placeholders (1. GET the package page)
- Shell with placeholders to First paint (2. About 0.15 s (made up))
- First paint to Reviews patch (3. Reviews ready at 1.6 s)
- Reviews patch to Price patch (4. Price ready at 2.8 s)
- Price patch to Page complete (5. Response ends)
- Browser asks for the page: Browser → Page server
- Shell with placeholders: Page server → Browser: header, photos, two slots
- First paint: Browser → Visitor: shell shows, price says Checking
- Reviews patch: Page server → Browser: template for reviews
- Price patch: Page server → Browser: template for price
- Page complete: Browser → Visitor: both slots filled, response ends
Which words do you need before you read the markup?
Six terms carry the rest of this guide. Each one maps to a part of the package page.
- Shell: the part of the page you can send at once, here the title, photos and itinerary.
- Placeholder: a named spot in the shell where late content will go, here one for the price and one for reviews.
- Processing instruction: the kind of marker used as a placeholder. It starts with a less-than sign and a question mark, then a word such as start, end or marker.
- Template element: an HTML element whose content the browser reads but does not show on the page.
- Patch: a template element with a for attribute. The browser moves its content into the placeholder with the same name.
- Flush: pushing what the server has written so far out to the network, instead of holding it back.
How does the template element's for attribute work?
You mark a spot with a named placeholder. Later in the same response, a template whose for attribute holds that name fills the spot. In Chromium 153, the template itself then disappears. Chromium is the open-source browser project that Chrome and Edge are built on.
Three kinds of placeholder
The HTML Standard says a processing instruction marks a location in a document for further processing. There are three kinds here: a marker for a single point, and a start and end pair around temporary content.
The table describes the package page's markup in words. A marker is written as a less-than sign, a question mark, the word marker, then name="reviews" and a closing greater-than sign.
| Piece | What you write | What the visitor sees |
|---|---|---|
| Price slot | A div, a plain box element, holding a start instruction named price, a paragraph saying Checking today's price, then an end instruction | The loading text, until the price patch lands |
| Reviews slot | A section holding one marker instruction named reviews | Nothing, until the reviews patch lands |
| Reviews patch | A template element with for set to reviews, holding the rating paragraph | The rating appears inside the reviews section |
| Price patch | A template element with for set to price, holding the price paragraph | The loading text is replaced by the price |
Two details are easy to miss. An end instruction is optional: without it, the patch replaces everything from the start instruction to the end of the containing element. A question mark just before the closing sign is also optional.
What the browser does when a patch arrives
In our tests on Chromium 153, the browser replaced the placeholder with the template's content. It then removed the placeholder and the template, so the DOM, the tree of elements the browser builds, held only the new content.
Patching also streams. In our test, the first half of a template showed in its slot while the second half was still on the way. Chrome's docs describe the same flow when they warn about moving placeholders.
Placeholders are not elements, so they can sit where an element cannot. In our test, a marker between table rows stayed inside the table. A div in the same spot was moved out, above the table.
Several patches can also feed one slot. If each patch ends with a fresh marker of the same name, the next patch lands after it. The site could stream reviews one at a time this way.
Which browsers support out-of-order HTML streaming in October 2026?
As of 19 October 2026, Chrome and Edge support it from version 150, and Opera from version 134, per MDN's compatibility data. Firefox and Safari do not ship it yet.
The Chromium schedule lists 30 June 2026 as Chrome 150's stable date. ChromeStatus records a positive position from Mozilla and a support position from WebKit. A position is a statement of intent, not a release date.
Without support, nothing throws an error, but the slots never fill. In our Chromium 149 test, each instruction became a comment and each template kept its content hidden. The price stayed on Checking today's price, and the reviews section stayed empty.
Beginners can stop here. You know the problem, the markup and where it works today. The rest of the guide is for engineers who will build it, test it and plan for other browsers.
How do you stream the slow sections from a Python server?
Send the shell at once, start the slow calls together, and write each template as its data lands. The server only writes text in order; the browser does the slotting.
The sketch uses only Python's standard library. ElementTree, the built-in library for building XML trees, builds each piece of HTML, so no markup is typed by hand. wsgiref, a tiny built-in web server, serves it. A generator, a function that hands out results one by one with yield, produces the chunks. Read package_page first; the functions above it build HTML or stand in for the slow services.
import time
import xml.etree.ElementTree as ET
from concurrent.futures import ThreadPoolExecutor, as_completed
from wsgiref.simple_server import make_server
def el(name, text=None, attrs=None, children=()):
node = ET.Element(name, attrs or {})
node.text = text
node.extend(children)
return node
def pi(kind, name=None):
return ET.ProcessingInstruction(kind, f'name="{name}"' if name else None)
def to_html(node):
return ET.tostring(node, encoding="unicode", method="html")
def get_price():
time.sleep(2.8) # slow pricing service (made up)
return "price", el("p", "Rs 14,500 for two adults, 2 nights")
def get_reviews():
time.sleep(1.6) # reviews service (made up)
return "reviews", el("p", "4.6 out of 5 from 812 travellers")
def package_page():
shell = el("main", children=[
el("h1", "Houseboat package, 2 nights"),
el("div", attrs={"class": "price"}, children=[
pi("start", "price"),
el("p", "Checking today's price..."),
pi("end"),
]),
el("section", attrs={"class": "reviews"}, children=[pi("marker", "reviews")]),
])
yield to_html(shell) # leaves at once, before any slow call
with ThreadPoolExecutor() as pool:
jobs = [pool.submit(get_price), pool.submit(get_reviews)]
for job in as_completed(jobs): # whichever finishes first
name, content = job.result()
yield to_html(el("template", attrs={"for": name}, children=[content]))
def app(environ, start_response):
start_response("200 OK", [("Content-Type", "text/html; charset=utf-8")])
return (part.encode() for part in package_page())
if __name__ == "__main__":
make_server("127.0.0.1", 8000, app).serve_forever()What just happened? The shell leaves first. as_completed hands back whichever slow call finishes first, so the reviews patch goes out at 1.6 seconds without waiting for the price. wsgiref flushes after every string the generator yields, so each chunk leaves at once.
The browser places the templates directly inside body, after the main element, so they can reach any placeholder. ElementTree also escapes text, so a review containing markup arrives as plain text.
Two limits are deliberate. The sketch skips the doctype line and the head that a real page sends first. And wsgiref serves one request at a time, so use it to try the idea, not in production.
Map the slow parts
List each section that waits on a slow service, here the price and the reviews.
Name the placeholders
Give each slow section a unique name. Use a start and end pair when you want loading text, and a marker when empty is fine.
Send the shell first
Write and flush everything that is ready, including every placeholder, before you wait on any slow service.
Start slow calls together
Run the slow calls in parallel, so the total wait is the slowest call, not the sum of them.
Write templates as data lands
Send each result as a template whose for value matches its placeholder, as a direct child of body.
End the response
After the last template, let the response end, so the browser knows the page is complete.
The package page server and its backends
Drag to exploreNotice that the shell never waits for the two slow services. Each answer becomes its own template in the same response.
Diagram connections:
- Visitor's browser to Page server (GET package page)
- Page server to Package cache (Read shell data)
- Page server to Reviews service (Fetch reviews in parallel)
- Page server to Pricing service (Fetch price in parallel)
- Package cache to Page server (Package data)
- Reviews service to Page server (Ratings data)
- Pricing service to Page server (Price data)
- Page server to Visitor's browser (Shell, then templates)
- Visitor's browser: Shows the shell, then applies each patch
- Page server: Streams the shell, then one template per result
- Package cache: Title, photos, itinerary in about 150 ms (made up)
- Reviews service: Ratings, about 1.6 s (made up)
- Pricing service: Checks free boats, about 2.8 s (made up)
How to check that it really streams
Start the server, then run this client in a second terminal. It prints when each chunk arrives, in seconds since the request, and which patch the chunk carried.
import re
import time
import urllib.request
start = time.monotonic()
with urllib.request.urlopen("http://127.0.0.1:8000/") as resp:
while chunk := resp.read1().decode():
names = re.findall(r'for="(\w+)"', chunk) or ["shell"]
print(f"{time.monotonic() - start:.1f} s", *names)On our machine with Python 3.13, it printed three lines: 0.0 s shell, then 1.6 s reviews, then 2.8 s price. If you see a single line at 2.8 s, something between you and the server is buffering. If your terminal sets an http_proxy variable, set no_proxy to 127.0.0.1 first, or urllib sends the request to that proxy.
Then open the page in Chrome 150 or later. Watch the loading text turn into the price. In our Chromium 153 test, polling every 50 milliseconds, the reviews showed at about 1.6 s and the price at about 2.8 s.
Why does a proxy sometimes bring the blank screen back?
Because a layer between your server and the browser may collect the response before passing it on. nginx, a common reverse proxy (a server that sits in front of your app and forwards requests to it), buffers proxied responses by default.
We put nginx 1.24 with default settings in front of the sketch. The check printed a single line at 2.8 s: the shell, reviews and price arrived together, so the visitor saw a blank screen again. It read 2.8 s reviews price, because the check names only the patches when the shell shares their chunk.
Turning proxy_buffering off for that route brought back three lines, within 0.1 s of the direct timings. nginx's docs also let the app send an X-Accel-Buffering header set to no, which switches buffering off for that response.
Check every other layer in the path the same way, such as a CDN (content delivery network) or middleware that compresses responses. Run the check against the public address your users hit, not only against the app server.
What rules limit where a template can patch?
A template can only fill placeholders inside its own parent element. Chrome's docs give security as the reason. MDN puts it as stopping one component from updating unrelated parts of the page.
We checked each rule below in Chromium 153 with a local streaming server.
- A template that is a direct child of body can reach the whole document, including the head. In our test, a late template replaced the page title this way.
- A template nested inside a div could not reach a marker outside that div. Its content stayed hidden.
- A template that arrived before its placeholder did nothing. Send every placeholder in the shell before any patch.
- A template whose name matched no placeholder kept its content hidden in the page, as MDN describes.
- Templates added later through innerHTML or insertAdjacentHTML did not patch existing content, and setHTML stripped the template out.
- When a script moved a placeholder while a patch was streaming, the rest of the patch kept flowing into the old spot.
Patches are ordinary HTML, so escaping still matters. Chrome's docs list sanitising while patching only as a possible future addition. Escape user text before it goes into a template, as the ElementTree sketch does.
What should you do for Firefox and Safari users in 2026?
Give them a page that still works. Put real fallback content inside each placeholder, load the polyfill, or do both.
GoogleChromeLabs publishes template-for-polyfill on npm, version 0.1.1 as of 19 October 2026. Its README says it applies new templates through a mutation observer, a browser feature that reports page changes, rather than during parsing.
Pause and predict: the reviews patch leaves the server at 1.6 s, so when does the reviews slot fill with the polyfill? In our Chromium 149 test, later than the README suggests. With version 0.1.1, both slots filled only when the response ended, at about 2.8 s, instead of at 1.6 s and 2.8 s.
The source code shows why. While the page loads, it holds back a patch from a template in body if the template or its placeholder has no element after it. Each of our templates arrived as the last element so far, and our reviews marker and price end marker had nothing after them. So both patches waited for the end. In a second test on Chromium 141, adding an element after only the placeholders, or only the templates, changed nothing. Adding both brought back about 1.6 s and 2.8 s.
This is undocumented as of 19 October 2026, so pin the version and retest after upgrades. The polyfill also differed from native Chromium 153 in two ways: it applied a template sent before its placeholder, and a template added later with insertAdjacentHTML patched existing content.
Choosing a fallback for each slow section
Drag to exploreDecide per section. Anything people need in order to buy should never depend on a patch that might arrive late or not at all.
Diagram connections:
- Native support? to Native patching (Yes)
- Native support? to Polyfill loaded (No)
- Polyfill loaded to Is the section critical? (Then check each section)
- Is the section critical? to Real fallback in the range (Yes)
- Is the section critical? to Loading text is fine (No)
- Native support?: Chrome and Edge 150 or later
- Native patching: Each slot fills as its template arrives
- Polyfill loaded: Slots may fill only when the response ends
- Is the section critical?: Price is critical, reviews are nice to have
- Real fallback in the range: For example, a link to a working price page
- Loading text is fine: Reviews can wait until the end
For the package page, the team keeps the polyfill and puts a working link to a separate price page inside the price range. If no patch ever arrives, the visitor can still reach the price.
How does out of order HTML streaming compare with client-side fetching and framework streaming?
All of them show the fast content early. They differ in how much JavaScript the visitor's browser must run and when the slow calls start.
| Approach | What the visitor sees first | JavaScript needed | Main trade-off |
|---|---|---|---|
| Wait for everything | A blank screen for about 2.8 s, then the full page | None | Simple, but the slowest service sets the speed for everyone |
| Client fetch after load | The shell, then spinners | Your own fetch and DOM code for each section | Slow calls start only after the page and script load |
| Framework streaming | The shell, then sections as they finish | The framework's inline scripts | Works across browsers today, but ties you to that framework |
| Templates with a for attribute | The shell, then sections as they finish | None in Chrome and Edge 150 or later | Firefox and Safari need a fallback or the polyfill |
With client-side fetching, the browser loads the page, runs a script, and only then asks for the price. That adds at least one extra round trip, a request and reply over the network, before the slow call begins.
Framework streaming also sends one response. We checked React 19.3.1's streaming server renderer: the late chunk arrived as a hidden div, followed by an inline script that moved it into place.
Chrome's docs list a heavy framework among the workarounds that in-order HTML pushed developers into. The native version does the same move inside the HTML parser, with no script.
What is coming next for declarative partial updates?
Next comes a set of JavaScript methods for inserting and streaming HTML into an existing page. As of 19 October 2026, the new ones are in testing behind a flag, not in a stable release.
Chrome's docs list six static methods, including setHTML and appendHTML. Each has a streaming twin, such as streamHTML and streamAppendHTML. ChromeStatus says these return a writable stream you can pipe HTML into.
In our Chromium 153 test, setHTML already worked without a flag. appendHTML, streamHTML and streamAppendHTML only existed with the experimental web platform features flag turned on.
Unsafe variants switch off the sanitizer, the part that strips harmful markup, by default. They also take a runScripts option, false by default, that decides whether inserted scripts run.
Developers can try the methods from Chrome 148 with that flag. Chrome's docs, last updated 8 September 2026, said launch was planned for Chrome 155. As of 19 October 2026, ChromeStatus lists Chrome 157 instead.
How do you explain out of order HTML streaming in a system design interview?
Frame it as a latency problem, then walk through the shell, the patches, the delivery path and the fallbacks.
State the problem
One slow backend, like a 2.8 second price service, blocks the whole HTML response and delays first paint, the moment anything appears.
Split fast from slow
Render the shell from cached data at once. Leave a named placeholder for each slow section.
Stream the patches
Call slow services in parallel and send each result as a template with a matching for value, in finishing order.
Explain the browser's part
In Chrome and Edge 150 or later, the HTML parser moves each patch into its placeholder without JavaScript.
Protect the delivery path
Say how you keep proxies from buffering, and how you would measure chunk arrival times in production.
Cover support and safety
Mention the same-parent rule, escaping user text, and real fallback content for Firefox and Safari.
Compare the options
Contrast it with client-side fetching and framework streaming, and say when you would pick each.
Questions people ask
- Is out of order HTML streaming the same as lazy loading?
- No. Lazy loading waits until the visitor needs something, like scrolling near an image, and then makes a new request. Out of order HTML streaming sends everything in the first response, just not in page order.
- Do I need JavaScript for out of order HTML streaming?
- Not in Chrome and Edge 150 or later. The browser's HTML parser finds each template, matches its for value to a placeholder and moves the content in. You only need JavaScript for the polyfill in browsers without support.
- What happens if a template arrives before its placeholder?
- In Chromium 153 it does nothing, and its content stays hidden. In our test, the polyfill did apply it, so the two paths behave differently. Always send the shell with every placeholder before any patch.
- Can I use out of order HTML streaming with React, Next.js or another framework?
- It is plain HTML, so any server that can stream a response can send it. ChromeStatus notes lots of involvement from framework and CMS (content management system) developers. React 19.3.1 still used its own inline scripts in our test, so check your framework's docs.
- Can a late template change the page title?
- Yes, if the template is a direct child of body. Wrap the title in a start and end pair in the head, then send the real title later. In our Chromium 153 test, this replaced the title. Templates nested deeper cannot reach the head.
- Is it safe to put user content inside a template?
- Yes, if you escape it first, as you would anywhere in HTML. Chrome's docs list sanitising while patching only as a possible future addition. The same-parent rule limits where a patch lands, but it does not clean harmful markup inside it.
- When will the streamHTML and streamAppendHTML methods be released?
- Not yet, as of 19 October 2026. They can be tested from Chrome 148 behind the experimental web platform features flag. Chrome's docs mentioned Chrome 155, and ChromeStatus now lists Chrome 157. Treat both as plans that can change.
Sources and further reading
- Declarative partial updates (Chrome for Developers, last updated 8 September 2026)
- Out of order streaming: shipping milestones and browser positions (ChromeStatus)
- Renewed HTML insertion and streaming methods: planned milestone (ChromeStatus)
- The template element, for attribute and patching rules (MDN Web Docs)
- HTML Standard, section 13.1.7: Processing instructions (WHATWG)
- HTML Standard: the template element and its for attribute (WHATWG)
- template-for-polyfill README, Limitations section (GoogleChromeLabs on GitHub)
- Chrome 150 release schedule, stable date (Chromium Dash)
- proxy_buffering directive (nginx documentation)