Suggest an editImprove this articleRefine the answer for “What is progressive rendering in web development”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Progressive rendering** displays page content as it becomes available, instead of waiting for the full document to load. ```html <img fetchpriority="high" src="hero.jpg" /> <!-- above-fold: immediate --> <img loading="lazy" src="product.jpg" /> <!-- below-fold: deferred --> ``` **Key point:** combine lazy loading, SSR streaming, and Suspense boundaries to cut FCP by 50-70% on slow networks.Shown above the full answer for quick recall.Answer (EN)Image**Progressive rendering** is a technique where page content appears as it becomes available, not after the full document loads. ## Theory ### TL;DR - Think of a newspaper printing page by page: you read the headline immediately while page 10 is still printing - Core idea: paint above-fold content first, defer everything below - Cuts Time to Interactive by 50-70% on slow networks - Use when FCP is above 1s or mobile bounce rates are high; skip for pages under 100KB - Main tools: `loading="lazy"`, `IntersectionObserver`, React `Suspense`, SSR streaming ### Quick Example ```html <!-- Above-fold: load immediately --> <img src="hero.jpg" fetchpriority="high" width="1200" height="600" /> <!-- Below-fold: defer until viewport --> <img loading="lazy" src="product.jpg" width="300" height="300" alt="Product" /> ``` ```jsx // React: show skeleton, render component when data arrives <Suspense fallback={<Skeleton />}> <HeavyDashboard /> </Suspense> ``` The browser paints the hero image right away. The product image only fetches when the user scrolls near it. The React component shows a skeleton until its data is ready. Three different tools, one result. ### Key Difference from Full-Page Loading With a standard load, the browser waits for the full HTML response before painting anything useful. With progressive rendering, it starts painting committed DOM nodes as the HTML parser receives chunks. The preloader thread scans ahead in the byte stream and kicks off resource fetches early (HTML spec, §12.2.6). Users see real content in under 200ms instead of a blank screen for 2-3 seconds. ### The Four Techniques **Lazy loading** defers images, videos, and components until they approach the viewport. The native `loading="lazy"` attribute covers most cases in Chrome 76+. For React components, `React.lazy()` with `Suspense` does the same. **SSR streaming** sends HTML in chunks from the server. React 18's `renderToPipeableStream` and Next.js App Router both support this. The browser renders the first chunk while the server is still generating the rest. A user dashboard where the profile header arrives in 200ms and the post feed follows at 800ms is a direct result of this approach. **Skeleton screens** replace empty space with approximate shapes of the incoming content. This keeps layout stable and signals to users that loading is happening. No sudden shifts, no blank stare. **Progressive hydration** attaches JavaScript behavior only to the parts users need first. The `<Script strategy="lazyOnload" />` pattern in Next.js fires third-party scripts after the page is interactive, not before. ### How Browsers Handle This The HTML parser processes bytes as they arrive and builds the DOM incrementally. Nodes go to the renderer as soon as they are complete. The preloader thread runs in parallel, scanning ahead for `<script>`, `<link>`, and `<img>` tags to start fetching early. In React 18, `Suspense` boundaries tell the server to skip a subtree that is not ready yet, flush the rest of the HTML, then stream the suspended subtree as a separate chunk when its data resolves. The client hydrates each chunk independently. That is why you can have a working header while the feed is still loading. ### When to Use - E-commerce grids with 50+ product images: `loading="lazy"` on everything below the fold - Dashboards with independent data sources: `Suspense` around each widget so fast data shows immediately - Content sites on Next.js: App Router streaming cuts FCP from 3s to under 1s - React SPAs with 50+ components: `React.lazy` + code splitting keeps the initial bundle small - Skip it for static pages under 100KB, and for pages where SEO crawlers need full HTML and you are not using SSR ### Common Mistakes **1. Lazy-loading the hero image.** ```html <!-- Wrong: delays LCP by 2 seconds or more --> <img loading="lazy" src="hero.jpg" /> <!-- Right: tell the browser this image matters --> <img fetchpriority="high" src="hero.jpg" /> ``` `loading="lazy"` removes the image from the preload scanner. On a hero image that is exactly what you do not want. **2. `React.lazy` without a `Suspense` boundary.** ```jsx // Wrong: unhandled promise, white screen for 3s const Chart = React.lazy(() => import('./Chart')); <Chart /> // Right <Suspense fallback={<Spinner />}> <Chart /> </Suspense> ``` Without the boundary, the rejected Promise bubbles up and crashes the render tree. **3. SSR streaming without error boundaries.** ```tsx // Wrong: fetch fails mid-stream, Node aborts the connection uncleanly async function UserData() { const data = await fetch('/api/user').then(r => r.json()); return <div>{data.name}</div>; } // Right: wrap with error.js (Next.js) or ReactErrorBoundary ``` Partial HTML is already sent when the error hits. Without an error boundary, the client receives broken markup and hydration fails. **4. Layout shift from lazy-inserted content.** Images loaded after paint push existing content down. Reserve the space upfront: ```css img { width: 100%; aspect-ratio: 16 / 9; /* holds space before image loads */ } ``` Or set explicit `width` and `height` on `<img>` tags. Both give the browser layout info before the image arrives, keeping CLS under 0.1. **5. Hydration mismatch with lazy images in SSR.** ```jsx // Wrong: server renders eager, client applies lazy - mismatch function Post({ post }) { return <img loading="lazy" src={post.image} />; } // React 18 warns and re-renders fully, wasting the streaming benefit // Right: use next/image for consistent server/client output import Image from 'next/image'; <Image src={post.image} priority={false} /> ``` ### Real-World Usage - Next.js 14 App Router: `Suspense` + React Server Components stream RSC payloads; Vercel's own dashboard runs on this - Remix v2: `defer()` streams slow data without blocking fast data on the same route - SvelteKit 2: `load` functions stream chunks, popular on content-heavy sites - Shopify Hydrogen: built on Remix's streaming model for product pages - YouTube: `IntersectionObserver` + `loading="lazy"` on thumbnail grids - Express / Node.js: `res.write()` streams HTML before all data is ready ### Follow-up Questions **Q:** What is the difference between progressive rendering and code splitting? **A:** Code splitting breaks JS into separate bundles that load on demand. Progressive rendering controls when DOM content appears. They combine naturally through `React.lazy()`: it code-splits the bundle, and `Suspense` progressively renders the result. **Q:** How does progressive rendering affect Core Web Vitals? **A:** Streaming critical HTML early cuts FCP and LCP. The risk is CLS: inserting content without reserved space shifts layout and hurts that score directly. **Q:** How do you lazy-load images without the native `loading="lazy"` attribute? **A:** Use `IntersectionObserver`. Watch the image element; when it enters the viewport, swap `data-src` into `src`. Works in Chrome 58+, Firefox 55+, and any environment that supports the Observer API. **Q:** How does React 18 streaming handle a `Suspense` boundary that never resolves? **A:** The server waits until a configured timeout, then flushes whatever is ready. In Next.js you control this via `loading.js`. Unresolved boundaries send the fallback HTML so the client never gets a blank response. **Q:** In React Server Components, how does streaming interact with the module graph at build time? **A:** RSC boundaries act as streaming split points. The server flushes each boundary independently as its data resolves. Next.js 14 with Turbopack prefetches RSC chunk boundaries in the background so sibling modules do not block each other. Internal Next.js benchmarks showed roughly 35% performance gain over webpack from this change. ## Examples ### Basic: Lazy Loading an Image Grid ```html <!DOCTYPE html> <html> <head> <title>Product Grid</title> </head> <body> <!-- Hero: paint immediately, reserve space --> <img src="hero.jpg" fetchpriority="high" width="1200" height="600" alt="Sale" /> <!-- Products: load as user scrolls, space reserved via width/height --> <div class="grid"> <img loading="lazy" src="prod1.jpg" width="300" height="300" alt="Product 1" /> <img loading="lazy" src="prod2.jpg" width="300" height="300" alt="Product 2" /> <img loading="lazy" src="prod3.jpg" width="300" height="300" alt="Product 3" /> </div> </body> </html> ``` The hero has `fetchpriority="high"` so the browser prioritizes it over other resources. The product images carry explicit `width` and `height` so the browser reserves their space before they arrive. No CLS, no delayed hero. This is the minimum setup for any image-heavy page. ### Intermediate: React 18 SSR Streaming (Next.js App Router) ```tsx // app/dashboard/page.tsx import { Suspense } from 'react'; async function Profile() { // Fast: resolves in ~100ms const user = await fetch('https://api.example.com/user').then(r => r.json()); return <div>{user.name}'s Dashboard</div>; } async function Posts() { // Slow: resolves in ~900ms const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>; } export default function Dashboard() { return ( <Suspense fallback={<div>Loading profile...</div>}> <Profile /> <Suspense fallback={<div>Loading posts...</div>}> <Posts /> </Suspense> </Suspense> ); } // Profile HTML streams at ~100ms. Posts stream at ~900ms. // Lighthouse FCP: 0.8s vs 3.2s with blocking SSR. ``` Two nested `Suspense` boundaries let each data source stream independently. The profile is not blocked by the posts API. If the posts fetch fails, only the inner boundary shows an error; the profile stays intact. I have seen this pattern cut dashboard FCP by more than half in production. ### Advanced: Hydration Mismatch with SSR Streaming ```jsx // Pattern that causes a React 18 hydration warning in production // Wrong: server renders the image eagerly by default. // Client component applies loading="lazy" after mount. // React sees a prop mismatch and re-renders the whole tree. function PostCard({ post }) { return ( <Suspense fallback={<div>Loading...</div>}> {/* Server: no loading attr. Client: loading="lazy". Mismatch. */} <img loading="lazy" src={post.coverImage} alt={post.title} /> </Suspense> ); } // Console: Warning: Prop `loading` did not match. // React discards streamed HTML and re-renders from scratch. // Right: next/image generates consistent markup on both sides import Image from 'next/image'; function PostCard({ post }) { return ( <Image src={post.coverImage} alt={post.title} width={800} height={400} priority={false} // same on server and client /> ); } ``` The mismatch happens because React's hydration compares server HTML against what the client would render. When they differ, React throws away the server HTML and re-renders from scratch. That discards the entire streaming optimization for this component tree. `next/image` with `priority={false}` produces the same output on both sides, so hydration passes cleanly.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.