Progressive Rendering
Waiting for a page to be fully ready before showing anything is one of the oldest and most persistent mistakes in web performance. Users do not need the whole page; they need the part that is on screen right now. Progressive rendering is the discipline of delivering exactly that: sending and displaying content as it becomes available, prioritising what is visible, and deferring everything else until it is needed.
This guide follows the strategy discussions in CSR vs SSR and hydration vs prerendering, and connects to the resource loading concerns in render-blocking resources.
What you'll learn
The core principle
Every second a user stares at a blank page is a second the browser has the data it needs but is choosing not to show it yet. Progressive rendering removes that choice.
Show work as it is done
The idea predates modern frameworks: HTTP chunked transfer encoding, introduced in HTTP/1.1, lets a server flush bytes to the client before the response body is complete. The browser starts parsing and painting those bytes immediately. The modern expression of the same principle is streaming SSR, where a framework flushes the document header, the above-the-fold HTML, and the critical CSS first, then streams in the slower, data-dependent sections as they resolve. Perceived performance, what users feel, improves enormously even when the total load time is unchanged, because something meaningful appears immediately.
Streaming HTML
Streaming is the server-side mechanism that makes progressive rendering possible. It is available in every modern Node.js framework and most edge runtimes.
Flushing early with Node.js streams
// Express: flush the shell immediately, stream the rest
app.get('/article/:slug', async (req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.write(shellHTML); // header + nav + critical CSS
var article = await db.get(req.params.slug);
res.write(renderArticle(article));
res.end(footerHTML);
});
The shell, the navigation, and the critical CSS land in the browser while the database query is still in flight. The browser parses and renders them immediately. When the article data arrives, the server flushes it, and the browser continues rendering. The user sees navigation and a skeleton within milliseconds of the first byte, not after all the data is gathered. React's renderToPipeableStream and similar APIs in other frameworks handle this automatically with Suspense boundaries as flush points.
Above-the-fold priority
Not all content is equally urgent. The viewport is the user's frame of attention, and everything above the fold needs to be fast. Everything below it can wait.
Prioritising the visible viewport
<!-- Hero image: eager, high priority, explicit dimensions -->
<img src="hero.webp" width="900" height="500"
loading="eager" fetchpriority="high"
alt="Article hero image">
<!-- Below-fold image: lazy, no fetch until near viewport -->
<img src="chart.webp" width="600" height="400"
loading="lazy"
alt="Data chart">
The hero image on a page is almost certainly the Largest Contentful Paint element. Marking it fetchpriority="high" tells the browser's preload scanner to fetch it at maximum priority, ahead of lower-priority resources. Giving it explicit dimensions prevents layout shift when it arrives. Every other image on the page benefits from loading="lazy", which defers the fetch until the image is near the viewport, reducing initial network contention and keeping the critical path clear.
Lazy loading images and components
Lazy loading is the client-side complement to server-side streaming: don't pay for resources until they are actually needed.
Deferring JavaScript and images
// Lazy-load a heavy component on interaction
var loadEditor = async function () {
var { Editor } = await import('./editor.js');
Editor.mount('#container');
};
document.querySelector('#edit-btn')
.addEventListener('click', loadEditor, { once: true });
Native loading="lazy" on images is the easiest win available and requires no JavaScript. For components, dynamic import() defers the network request and parse cost until the user actually needs the feature. An IntersectionObserver can trigger the import when a section scrolls into view, matching the pattern in the scalable UI guide's list virtualisation discussion. Together these techniques mean the browser's initial work is proportional to what the user can see, not the full page.
Skeleton screens
When data is in flight, a blank space is worse than a placeholder. Skeleton screens tell users that content is coming and roughly where it will land.
Perceived performance over raw speed
/* Skeleton pulse animation */
.skeleton {
background: linear-gradient(90deg,
#e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
background-size: 200% 100%;
animation: shimmer 1.4s infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
A shimmer skeleton that matches the shape of the incoming content signals progress and reduces the perceived wait time, even when the actual load time is identical. Avoid spinners centred in a blank screen: they suggest indefinite waiting rather than structured progress. The skeleton is the placeholder commitment, telling the user exactly how much space the content will occupy, which also prevents layout shift when the real data lands.
Chunked server delivery
All of the above techniques work best when the server commits to sending partial responses early rather than buffering everything and sending it at once.
Early flush as an architectural commitment
The practical requirement is that your server or edge function does not buffer the entire response before writing to the socket. Node.js streams, Deno's ReadableStream, and Cloudflare Workers all support early flushing natively. The trap is middleware that buffers, compression libraries that hold the response, or template engines that accumulate the full string before handing it to the framework. Auditing the response pipeline to remove accidental buffering is as important as writing the streaming code. When the pipeline is clean, progressive rendering techniques stack on top naturally: stream the shell, stream the above-fold content, defer the below-fold via lazy load, and fill placeholders with skeletons as each chunk resolves.
Frequently Asked Questions
What is progressive rendering?
Progressive rendering is the practice of sending and displaying content to the user as it becomes available rather than waiting for the full page to be ready. It includes techniques like streaming HTML, lazy loading below-the-fold resources, and prioritising above-the-fold content for faster perceived load times.
What is streaming HTML?
Streaming HTML sends the document to the browser in chunks as the server processes it. The browser can start parsing and rendering the first chunk while the server is still generating the rest. This improves Time to First Byte and allows above-the-fold content to paint before slower data-driven sections are ready.
How does lazy loading improve rendering performance?
Lazy loading defers loading resources that are not yet visible to the user. Images and iframes with loading="lazy" only download when they approach the viewport. JavaScript components can be loaded on demand using dynamic import. This reduces the initial payload and lets the browser focus its work on what the user can actually see.
What are skeleton screens?
Skeleton screens are placeholder layouts that match the shape of content before it loads. They give users a sense of structure and progress while data is in flight, which feels faster than a blank space or a spinner even when the actual load time is identical.
Read next: the Rendering hub, or continue to hydration vs prerendering for the server-side strategies that progressive rendering builds on.
Want to see progressive view rendering in action?
The Backbone guide shows how to render views progressively as model data arrives.
Explore the Backbone Guide →