Suggest an editImprove this articleRefine the answer for “SSR and lazy loading of components”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)Before React 18, `React.lazy()` effectively blocked rendering on the server, because classic SSR (`renderToString`) cannot wait for promises; in React 18+ with Streaming SSR, the server can "suspend" the render of a lazy component, send the `<Suspense>` fallback, and stream in the ready content later. **Key point:** `React.lazy` cannot be used without `<Suspense>`, otherwise SSR will "hang".Shown above the full answer for quick recall.Answer (EN)Image## Reminder: what SSR and lazy loading are | Mechanism | What it does | |---|---| | **SSR (Server-Side Rendering)** | Generates HTML on the server, so the page renders immediately (without waiting for JS). | | **Component Lazy Loading** | Loads a component (or its code) **only when needed** - so the whole bundle isn't pulled in at once. | --- ## How they interact When you use `React.lazy()` or `dynamic import()` in an SSR app: 1. The server starts **rendering the React components**. 2. If it runs into a **lazy component** that isn't loaded yet (its code is split into a separate JS chunk), the server cannot render it right now. 3. React either: - **stops the render** (in classic SSR); - or **shows the fallback** from `<Suspense>` and keeps streaming (in Streaming SSR); - or **loads the module synchronously**, if it's already in memory (in dev mode). --- ## 1. In classic SSR (before React 18) `React.lazy()` effectively **blocks rendering** if its module isn't loaded yet. For example: ```javascript const LazyComp = React.lazy(() => import('./BigChart')); function Page() { return ( <div> <Header /> <LazyComp /> {/* SSR gets "stuck" here */} </div> ); } ``` On the server, `import('./BigChart')` is a **Promise**, but classic SSR (`renderToString`) cannot wait for promises. The result: - The server throws an error: *"A component suspended while rendering, but no fallback UI was specified."* - Or it simply cannot finish the SSR. So before React 18, **lazy components didn't work on the server** - they had to be loaded synchronously or SSR had to be turned off for them (`ssr: false` in Next.js). --- ## 2. In React 18 and Streaming SSR React 18 completely changed the rules of the game. Now SSR **knows how to work with Suspense and lazy components** asynchronously. ### What happens: 1. The server starts rendering HTML. 2. When React runs into a lazy component, it **"suspends" the render** of that part. 3. It streams out the fallback HTML (for example, `<Skeleton />`). 4. When the module loads, React **continues streaming** and inserts the ready content later. Example: ```javascript import { Suspense, lazy } from 'react'; const LazyComp = lazy(() => import('./BigChart')); export default function Page() { return ( <div> <Header /> <Suspense fallback={<p>Loading chart...</p>}> <LazyComp /> </Suspense> </div> ); } ``` In Streaming SSR: - `<Header />` and `<p>Loading chart...</p>` render and are **sent to the client immediately**; - when `BigChart` loads, the server **sends** the rest of the HTML. This makes SSR **asynchronous, smooth, and "lazy" by nature**. --- ## 3. In Next.js In Next.js (from version 13, App Router), this is built in out of the box: ```javascript import dynamic from 'next/dynamic'; const LazyChart = dynamic(() => import('./Chart'), { ssr: true, // or false, if you don't want to render on the server loading: () => <p>Loading...</p>, }); export default function Page() { return ( <> <Header /> <LazyChart /> </> ); } ``` - `ssr: true` → the component will be rendered on the server (through Suspense). - `ssr: false` → the component is fully excluded from SSR and renders only on the client. This gives flexibility: you can choose "lazy, but with SSR" or "lazy, but browser-only". --- ## Advantages of this interaction | Advantage | Description | |---|---| | Faster first render | SSR sends HTML immediately, even if some components load lazily | | Smaller JS bundle | Lazy loading reduces the initial bundle size | | Smooth content loading | Through `Suspense`, SSR sends a fallback, then replaces it with the ready component | | Support for async data | Lazy components can load data from an API without blocking the whole SSR | --- ## Potential difficulties | Problem | Description | |---|---| | `React.lazy` cannot be used without `Suspense` | Otherwise SSR "hangs" | | The fallback can stay longer than expected | If the import is heavy or the network is slow | | The old `renderToString` doesn't support Suspense | You need `renderToPipeableStream()` | | Chunk caching | The CDN needs to be configured so lazy components' JS chunks load correctly | --- ## A full SSR + lazy loading + Suspense example ```javascript import { Suspense, lazy } from 'react'; import { renderToPipeableStream } from 'react-dom/server'; const LazySection = lazy(() => import('./Section')); function App() { return ( <div> <Header /> <Suspense fallback={<LoadingSection />}> <LazySection /> </Suspense> </div> ); } // Server app.get('/', (req, res) => { const stream = renderToPipeableStream(<App />, { onShellReady() { res.setHeader('Content-Type', 'text/html'); stream.pipe(res); }, }); }); ``` The client gets the header and `LoadingSection` immediately, and `Section` gets added to the DOM later, once it's ready. --- ## Conclusion **SSR and lazy loading now combine really well** - thanks to React 18 and Streaming SSR: | Criterion | Old SSR | Streaming SSR | |---|---|---| | Do lazy components work? | No | Yes | | Is Suspense supported? | No | Yes | | Can content be sent in parts? | No | Yes | | UX without a "blank screen"? | Not always | Always |For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.