skip to content

A React page is server-rendered with `renderToPipeableStream`, but its content is fetched inside `useEffect`. What does the HTML the server sends actually contain, and what does that cost you?

level: seniorimportance: should knowfreq 48%

answer

  1. render phase only, no commit
  2. effects never run on the server
  3. the skeleton is what you shipped
  4. LCP waits for bundle plus hydration
  5. fetch where render can await

basics

~20 s

Effects never run during server rendering, so the streamed HTML contains only the empty or skeleton state. The request starts after the bundle downloads and hydrates, meaning server rendering buys nothing for that data and non-executing crawlers see no content.

solid answer

~50 s

Server rendering runs only the render phase — component functions execute, but there is no DOM and no commit, so `useEffect` and `useLayoutEffect` callbacks are skipped entirely. The markup you stream is therefore whatever the component renders in its initial empty state: a spinner or skeleton. The real content is gated behind downloading the JS bundle, hydrating with `hydrateRoot`, committing, and only then a network round trip — so you paid for SSR and got none of its benefit for that data. The costs are concrete: a late largest contentful paint, layout shift when the skeleton is replaced, and no content at all for a crawler or link-preview bot that does not execute JavaScript. The fix is to fetch where render can await it — a server component or a route-level loader — and wrap the slow part in a `Suspense` boundary so the shell streams immediately.

go deeper

for a junior

Remember the rule itself: effects do not run on the server, so anything fetched in an effect is missing from the server-rendered HTML. Be able to say the page ships its loading state.

for a middle

Trace the full timeline — stream shell, paint skeleton, download bundle, hydrate, commit, then fetch — and explain why server rendering delivers no benefit for that data. Know that the component body still runs on the server.

for a senior

Quantify the damage: LCP measured on real content, layout shift when the skeleton is replaced, empty social previews, and server compute spent producing a shell. Propose fetching during render with a Suspense boundary placed so a slow query delays only its own subtree.

for a principal

Decide per route whether server rendering is earning its cost at all, and set the contract for what must appear in the initial HTML — indexable content, LCP element, above-the-fold state — versus what may legitimately arrive after hydration.

## What server rendering actually executes On the server, React runs the render phase and nothing else. It calls your component functions, collects the elements they return, and serialises the result to an HTML stream. There is no DOM to commit to, no browser to paint, and no lifetime after the response is written — so there is no commit phase, and passive and layout effects are never invoked. `useState` initialisers, `useMemo` factories and the component body all run; `useEffect` bodies and their cleanups do not. That single fact determines the whole answer. Any data that only arrives via an effect cannot be in server-rendered HTML, by construction. ## The timeline the user actually experiences 1. The server streams HTML containing a skeleton, because that is what the component renders with `data === null`. 2. The browser paints the skeleton — fast, and misleading. 3. The JS bundle downloads and parses. 4. `hydrateRoot` attaches React to the existing markup and commits. 5. Effects run; the request finally leaves the browser. 6. The response arrives, state updates, the real content replaces the skeleton. Steps 3 to 6 are entirely serial and happen after the server has already done work. On a slow connection the gap between the first paint and useful content can be several seconds. ## What it costs **Perceived performance.** The skeleton usually is not the largest contentful paint candidate you want; LCP lands on the real content, which is now several steps away. You get an early first paint that measures well on naive metrics and badly on the ones that reflect user experience. **Layout stability.** Replacing a placeholder with content of a different size shifts the page, hurting cumulative layout shift, unless you reserve exact dimensions. **Indexability and previews.** Search crawlers that execute JavaScript may still index the page, but on a delay and with no guarantee. Link-preview and social-card bots generally do not execute JavaScript at all, so they see the skeleton — which is why an otherwise fine app produces empty previews when shared. **Wasted server work.** You are paying for server rendering and getting only a shell out of it. That is a legitimate argument for either fixing the data path or dropping SSR for that route. ## Getting data into the server-rendered HTML The requirement is to have the data *before or during* render rather than after commit. Three shapes do that: - **A server component** is an async function component that runs only on the server and can `await` during render. Its output is already resolved when the HTML is produced, so the markup contains real content and no client effect is involved. - **A route loader** resolves the route's data before the matched components render, then renders with data in hand. - **A render-time resource read**, where the component reads a promise created outside it and the framework suspends until it settles, so the streamed output waits for real data rather than committing an empty state. In all three, `Suspense` is what makes it non-blocking: `renderToPipeableStream` streams the shell immediately and sends the slow subtree's HTML later when it resolves, so a slow query delays only its own boundary. That is the structural difference from effect fetching — the loading state is expressed as a boundary in the tree rather than as a boolean the component reached only after committing empty. ```jsx <Suspense fallback={<Skeleton />}> <SlowSection /> </Suspense> ``` ## The compromise you will be asked about A long-standing middle path is for the server to fetch the data, embed it in the HTML as a serialised payload, and have the client seed its initial state from it so no effect fetch is needed on first load. It works and is worth naming, but be honest about the cost: you maintain the serialisation format, the payload inflates the document, and you must keep the server's fetch and the client's refetch semantics in sync. It is the reason the ecosystem moved toward fetching during render instead.

  • If effects do not run on the server, what parts of a component do run there?
    The component body itself — including `useState` initialisers, `useMemo` factories, and the JSX evaluation — plus anything React needs to produce markup. What is skipped is everything tied to the commit and to a living DOM: `useEffect`, `useLayoutEffect`, refs attaching to real nodes, and all cleanups. That is why any browser-only API touched in the body, rather than in an effect, breaks server rendering.
  • Would moving the fetch to useLayoutEffect put the data in the server HTML?
    No. Layout effects run earlier than passive effects, but both run during or after the commit phase, and server rendering has no commit at all — so neither is invoked. React will also warn that `useLayoutEffect` does nothing on the server. The only way to have data in the streamed markup is to resolve it before or during render, not after.
  • Where does a Suspense boundary go so that a slow query does not delay the whole page?
    Around the smallest subtree that actually needs the slow data, with everything above it renderable from data you already have. `renderToPipeableStream` flushes the shell immediately and streams that boundary's HTML when it resolves. Put the boundary too high and you have blocked the whole page on the slowest query; put one around every leaf and the page fills in as a distracting patchwork of independent skeletons.
  • When is client-side fetching in an effect acceptable on a server-rendered page?
    For content that is not part of the initial meaningful view and does not need to be indexed: personalised widgets below the fold, data that depends on browser-only state, or panels behind an interaction. The test is whether the HTML would be judged incomplete without it. If a crawler or a link preview should see it, or it is the LCP element, it belongs in the server-rendered output.

saying these in an interview costs you the question

  • Says effects run during server rendering, just later
  • Believes hydration re-runs the render on the server
  • Assumes crawlers always execute the page's JavaScript
  • Thinks useLayoutEffect executes on the server
  • Claims a fast first paint means good perceived performance

context