- Published on
Dynamic HTML Streaming: The Forgotten Performance Superpower
- Authors

- Name
- Andrii Cherevatenko
We often overcomplicate web performance. When a page feels slow, the modern trend is to adopt a complex Single Page Application (SPA) architecture, move everything to the client, and show a loading spinner. We assume that to make an interface feel "instant," we need React, Vue, or Svelte handling the rendering on the user's device.
But there is a simpler way. You can achieve immediate loading states, instant First Contentful Paint (FCP), and a responsive experience using standard HTML and your existing monolith server - without the complexity of client-side hydration or API management.
This technique is HTML Streaming. It is how platforms like Facebook and GitHub delivered dynamic content for years before React Server Components existed, and it remains one of the most efficient ways to maximize performance on the web.
The "White Screen" Problem
Consider a typical scenario: you are building a user dashboard that needs to display a revenue report. The database queries and business logic needed to generate that report take about 2 seconds.
In a traditional synchronous request cycle, the user clicks "Dashboard," and the browser sends a request. Your server receives it, halts, and waits for the data processing. For two full seconds, the server sends nothing back. The user stares at a white screen.
This is a disaster for perceived performance. In my previous analysis of the 14KB Rule, we discussed how the initial TCP packet window (usually around 14KB) is the most critical moment for performance. By waiting for the database, you are leaving that window completely empty. You are wasting the opportunity to paint the navigation, the header, and the page structure while the heavy lifting happens in the background.
Stop Buffering, Start Streaming
The solution isn't to rewrite your app in a client-side framework; it's to stop buffering the response.
We can take advantage of the fact that HTTP is a stream. We don't have to send the entire installation of HTML at once. We can flush the "App Shell" - the static <head>, CSS, navigation, and layout skeleton - immediately.
Here is what that looks like in a simple Node.js server. Notice that we write to the response stream instantly:
const http = require('node:http')
const server = http.createServer((req, res) => {
// 1. Send headers immediately
// Browsers see this and prepare to render
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
// 2. Flush the Shell
// This HTML fits easily within the first TCP packet.
// The user sees the page structure instantly.
res.write(`
<!DOCTYPE html>
<html>
<head>
<title>Streaming Demo</title>
<style>
body { font-family: system-ui; padding: 20px; }
.skeleton { background: #eee; height: 100px; border-radius: 4px; }
</style>
</head>
<body>
<nav>My Dashboard</nav>
<h1>User Statistics</h1>
<!-- THE PLACEHOLDER -->
<div id="stats-container">
<div class="skeleton">Loading heavy data...</div>
</div>
`)
// The connection remains open while we fetch data...
fetchHeavyData(res)
})
At this exact moment, the user perceives the page as "loaded." The navigation is visible, the CSS is applied, and there is a skeleton box indicating that data is on the way. The Time to First Byte (TTFB) is effectively zero.
The "Out-of-Order" Swap
Now comes the magic. The server connection is still open, hanging on that fetchHeavyData call. Once the database returns our data 2 seconds later, we need to put it into that #stats-container div.
But we can't just append it to the HTML body, or it would appear at the bottom of the page, below the footer.
We use a technique called Out-of-Order Streaming. We stream the new content inside a hidden div at the bottom of the document, followed immediately by a tiny inline script tag. This script executes the moment it arrives in the browser, grabbing the new content and swapping it into the correct placeholder.
function fetchHeavyData(res) {
// Simulate the 2s database delay
setTimeout(() => {
const data = { revenue: '$50,000', visitors: '12,000' }
// 3. Stream the content + The Swap Script
res.write(`
<div id="streamed-content" style="display:none">
<div class="stats-card">
<p>Revenue: ${data.revenue}</p>
<p>Visitors: ${data.visitors}</p>
</div>
</div>
<script>
// The Mechanism: Swap placeholder with real content
document.getElementById('stats-container').innerHTML =
document.getElementById('streamed-content').innerHTML;
</script>
</body>
</html>
`)
// 4. Finally close the connection
res.end()
}, 2000)
}
This effectively mimics the behavior of React Suspense or expensive hydration logic, but it uses vanilla DOM manipulation that has worked in browsers since the 90s.
Why This Works (The Browser Perspective)
You might wonder if this confuses the browser. It doesn't. HTML parsers are incredibly resilient and are designed to handle streams.
When the browser receives the first chunk, it parses the DOM tree up to the placeholder. It doesn't wait for the closing </html> tag to start painting pixels. It draws what it has.
Two seconds later, when the second chunk arrives, the parser wakes up, parses the new div and the <script> tag, and executes JavaScript immediately. Because the swap happens synchronously on the main thread, the user sees the data "pop" into place exactly where it belongs.
What About Robots?
A common fear with streaming or JavaScript-based loading is SEO. Does Googlebot see the content?
Yes, it does. Googlebot and other modern crawlers wait for the network connection to close. They see the Transfer-Encoding: chunked header and keep the line open until your server sends res.end(). Once the stream is finished, Google's Web Rendering Service executes the utility script, swaps the content, and indexes the final page state.
For "dumber" bots that don't execute JavaScript - like link previewers for Slack, Discord, or Twitter - you can include a <noscript> block in the second chunk containing the raw text data. This ensures your content is visible to everyone, regardless of capability.
Conclusion
We tend to think of "Modern Web Development" as a choice between a static HTML page that feels clunky or a massive JavaScript application that feels app-like.
HTML Streaming sits comfortably in the middle. It gives you the "app-like" feel of instant loading and skeletons, but keeps the architecture simple and server-driven. You solve the 14KB rule constraints, you keep your Time to First Byte low, and you don't have to ship a megabyte of JSON and library code just to show a dashboard.
Sometimes the best "new" performance tricks are just standard HTTP features we forgot to use.