docs/source/performance/server-side-rendering.mdx
Server-side rendering (SSR) is a performance optimization for modern web apps. It enables you to render your app's initial state to raw HTML and CSS on the server before serving it to a browser. This means users don't have to wait for their browser to download and initialize React (or Angular, Vue, etc.) before content is available:
sequenceDiagram
participant Browser;
participant Server;
Browser->>Server: Requests example.com;
Note over Server: Renders initial state of example.com on server;
Server->>Browser: Returns raw HTML and CSS;
Note over Browser: Displays raw HTML and CSS;
Note over Browser: Initializes view layer and "rehydrates" it with returned data
Apollo Client provides a handy API for using it with server-side rendering, including a function that executes all of the GraphQL queries that are required to render your component tree. You don't need to make any changes to your queries to support this API.
When you render your React app on the server side, most of the code is identical to its client-side counterpart, with a few important exceptions:
You need to use a server-compatible router for React, such as React Router.
(In the case of React Router, you wrap your application in a StaticRouter component instead of the BrowserRouter you use on the client side.)
You need to replace relative URLs with absolute URLs wherever applicable.
The initialization of Apollo Client changes slightly, as described below.
Here's an example server-side initialization of Apollo Client:
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
const client = new ApolloClient({
ssrMode: true,
link: new HttpLink({
uri: "http://localhost:3010",
credentials: "same-origin",
headers: {
cookie: req.header("Cookie"),
},
}),
cache: new InMemoryCache(),
});
Provide ssrMode: true to prevent Apollo Client from polling on the server. This setting also tells the client to prioritize cache values over network requests when possible.
You also might need to configure your GraphQL endpoint to accept GraphQL operations from your SSR server (for example, by safelisting its domain or IP). Use absolute URLs for your GraphQL endpoint on the server, because relative network requests can only be made in a browser.
It's possible and valid for your GraphQL endpoint to be hosted by the same server that's performing SSR. In this case, Apollo Client doesn't need to make network requests to execute queries. For details, see Avoiding the network for local queries.
Let's look at an example of SSR in a Node.js app. This example uses Express and React Router v4, although it can work with any server middleware and any router that supports SSR.
First, here's an example app.js file, without the code for rendering React to HTML and CSS:
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { ApolloProvider } from "@apollo/client/react";
import Express from "express";
import React from "react";
import { StaticRouter } from "react-router";
// File shown below
import Layout from "./routes/Layout";
const app = new Express();
app.use((req, res) => {
const client = new ApolloClient({
ssrMode: true,
link: new HttpLink({
uri: "http://localhost:3010",
credentials: "same-origin",
headers: {
cookie: req.header("Cookie"),
},
}),
cache: new InMemoryCache(),
});
const context = {};
// The client-side App will instead use <BrowserRouter>
const App = (
<ApolloProvider client={client}>
<StaticRouter location={req.url} context={context}>
<Layout />
</StaticRouter>
</ApolloProvider>
);
// TODO: rendering code (see below)
});
app.listen(basePort, () =>
console.log(`app Server is now running on http://localhost:${basePort}`)
);
So far, whenever this example server receives a request, it first initializes Apollo Client and then creates a React tree that's wrapped with the ApolloProvider and StaticRouter components. The contents of that tree depend on the request's path and the StaticRouter's defined routes.
It's important to create an entirely new instance of Apollo Client for each request. Otherwise, your response to a request might include sensitive cached query results from a previous request.
</Caution>prerenderStaticYou can instruct Apollo Client to execute all of the queries executed by the useQuery or the suspenseful query hooks (like useSuspenseQuery and useBackgroundQuery) in the React tree's components with the prerenderStatic function.
This function rerenders your React tree until no more network requests are made. When you use suspenseful hooks with a suspense-ready rendering function, the tree is rendered once and suspends while network requests are executed. When you use non-suspenseful hooks (like useQuery), this function renders all components, waits for all requests to finish, and then re-renders the tree until no more requests are made.
The function returns a Promise that resolves when all result data is ready in the Apollo Client cache and the final render is complete.
prerenderStatic supports multiple React rendering functions:
prerender from react-dom/static - Recommended for Deno or modern edge runtimes with Web Streams. Supports React Suspense.prerenderToNodeStream from react-dom/static - Recommended for Node.js. Supports React Suspense.renderToString from react-dom/server - Legacy API without Suspense support. Won't work with suspenseful hooks.renderToStaticMarkup from react-dom/server - Legacy API without Suspense support. Slightly faster than renderToString, but the result cannot be hydrated.The following code replaces the TODO comment within the app.use call in the example above:
// Add these imports to the top of the file
import { prerenderStatic } from "@apollo/client/react/ssr";
import { prerenderToNodeStream } from "react-dom/static";
// Replace the TODO with this
prerenderStatic({
tree: App,
// this is optional if your `App` component contains an <ApolloProvider>
context: { client },
renderFunction: prerenderToNodeStream,
}).then(async ({ result }) => {
// Extract the entirety of the Apollo Client cache's current state
const initialState = client.extract();
// TODO: Send the response to the client (see below for examples)
});
After prerenderStatic completes, you need to send the HTML response to the client. The approach depends on the rendering function you chose.
renderToPipeableStreamFor Node.js environments, you can stream the response to the client using renderToPipeableStream. This allows the browser to start displaying content before the entire page is rendered:
import { renderToPipeableStream } from "react-dom/server";
// After prerenderStatic completes
prerenderStatic({
tree: App,
context: { client },
renderFunction: prerenderToNodeStream,
}).then(async ({ result }) => {
const initialState = client.extract();
// Render the app again with streaming, injecting the Apollo state
const { pipe } = renderToPipeableStream(
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">
<App />
</div>
</body>
</html>,
{
bootstrapScriptContent: `window.__APOLLO_STATE__=${JSON.stringify(
initialState
).replace(/</g, "\\u003c")}`,
bootstrapScripts: ["/client.js"],
onShellReady() {
// Start streaming the response to the browser
res.setHeader("Content-Type", "text/html");
res.statusCode = 200;
pipe(res);
},
onError(error) {
console.error("Rendering error:", error);
},
}
);
});
renderToString for synchronous renderingFor simpler use cases or environments without stream support, you can use renderToString to render the entire page synchronously:
import { renderToString } from "react-dom/server";
// After prerenderStatic completes
prerenderStatic({
tree: App,
context: { client },
renderFunction: renderToString,
}).then(async ({ result }) => {
const initialState = client.extract();
// Create a complete HTML document with the cache state
const Html = () => (
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root" dangerouslySetInnerHTML={{ __html: result }} />
<script
dangerouslySetInnerHTML={{
__html: `window.__APOLLO_STATE__=${JSON.stringify(
initialState
).replace(/</g, "\\u003c")};`,
}}
/>
<script src="/client.js" />
</body>
</html>
);
// Render to string and send
const html = renderToString(<Html />);
res.setHeader("Content-Type", "text/html");
res.status(200);
res.send(`<!DOCTYPE html>${html}`);
res.end();
});
prerenderStatic provides several options to customize the rendering process:
You can enable diagnostics to detect inefficient rendering structures (like useQuery waterfalls) in your app:
const { diagnostics } = await prerenderStatic({
tree: App,
context: { client },
renderFunction: prerenderToNodeStream,
diagnostics: true,
});
console.log(`Rendered ${diagnostics.renderCount} times`);
// If renderCount is high, consider using fragment colocation
Timeout support
You can use an AbortSignal to stop the render loop early:
const signal = AbortSignal.timeout(2000); // 2 second timeout
const { result, aborted } = await prerenderStatic({
tree: App,
context: { client },
renderFunction: (tree) => prerenderToNodeStream(tree, { signal }),
signal,
});
if (aborted) {
console.log("Render timed out, returning partial result");
}
If you have deep useQuery waterfalls, you can increase the maxRerenders option (default: 50):
await prerenderStatic({
tree: App,
context: { client },
renderFunction: prerenderToNodeStream,
maxRerenders: 100,
});
Although the server-side cache's state is available in __APOLLO_STATE__, it isn't yet available in the client-side cache. InMemoryCache provides a helpful restore function for rehydrating its state with data extracted from another cache instance.
In your client-side initialization of Apollo Client, you can rehydrate the cache like so:
const client = new ApolloClient({
cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
uri: "https://example.com/graphql",
});
Now when the client-side version of the app runs its initial queries, the data is returned instantly because it's already in the cache!
prerender and resumeAndPrerenderIf you work with node streams, use prerenderToNodeStream and resumeAndPrerenderToNodeStream instead of prerender and resumeAndPrerender.
The APIs in this section require React 19.2.0 or later.
</Note>Using the resume function from react-dom/server, you can prerender some parts of your application (e.g., data common to all users) and then resume SSR for the remaining parts (e.g., user-specific data) later.
Start by using prerender to render the initial HTML and execute initial queries:
const client = new ApolloClient({
cache: new InMemoryCache(),
link,
});
const controller = new AbortController();
const promise = prerenderStatic({
tree: App,
context: { client },
renderFunction: (tree) =>
renderFunction(tree, {
signal: controller.signal,
}),
signal: controller.signal,
});
When all fetchable data is ready, call controller.abort() to stop the initial render and save the result:
controller.abort();
const { result, renderFnResult, aborted } = await promise;
If aborted is false, the render completed and you can send the result. If aborted is true, save the current render result, postponed work, and cache state to resume later:
const initialResponse = result; // HTML up to this point
const postponed = renderFnResult.postponed; // Postponed render work
const cacheState = client.extract(); // Cache state after initial render
To resume rendering (e.g., in a subsequent request with user-specific data):
const client = new ApolloClient({
cache: new InMemoryCache(),
link,
});
// Restore the cache state from the first render
client.cache.restore(cacheState);
const controller = new AbortController();
const promise = prerenderStatic({
tree: App,
context: { client },
renderFunction: (tree) =>
resumeAndPrerender(tree, postponed, {
signal: controller.signal,
}),
signal: controller.signal,
});
You can either let the render complete or abort it again for another partial result:
controller.abort(); // or let it complete
const { result, renderFnResult, aborted } = await promise;
If aborted is false, the render completed and you can send the result. If aborted is true, save the current render result and postponed work, then repeat the resume step as needed.
[!NOTE] The
resultfromresumeAndPrerenderdoes not include HTML from previous renders. You must concatenate the HTML strings from all partial renders to get the complete output.
If some of your initial queries use the network-only or cache-and-network fetch policy, you can provide the ssrForceFetchDelay option to Apollo Client to skip force-fetching those queries during initialization. This way, even those queries initially run using only the cache:
const client = new ApolloClient({
cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
link,
ssrForceFetchDelay: 100, // in milliseconds
});
If your GraphQL endpoint is hosted by the same server that you're rendering from, you can optionally avoid using the network when executing your SSR queries. This is particularly helpful if localhost is firewalled in the server's environment (e.g., on Heroku).
When creating an Apollo Client on the server, use a SchemaLink instead of an HttpLink. SchemaLink uses your schema and context to run the query immediately, without the need for a network request:
import { ApolloClient, InMemoryCache } from "@apollo/client";
import { SchemaLink } from "@apollo/client/link/schema";
// ...
const client = new ApolloClient({
// Instead of HttpLink use SchemaLink here
link: new SchemaLink({ schema }),
cache: new InMemoryCache(),
});
If you want to prevent a particular query from executing during SSR, use ssr: false in that query's options. This is useful for queries that should only run on the client side, such as user-specific data that isn't available during SSR.
When ssr: false is set, the component receives a result with loading: true, dataState: "empty", and data: undefined during server-side rendering. The query will execute normally once the component hydrates on the client.
function ClientOnlyUser() {
const { loading, data } = useQuery(GET_USER_WITH_ID, { ssr: false });
if (loading) {
return <span>Loading...</span>;
}
return <span>User: {data?.user?.name || "Not loaded"}</span>;
}
ssr: false behaves differently than skip: true, which prevents the query from executing on both the server and client until skip is set to false. During SSR, skip: true results in loading: false and networkStatus: NetworkStatus.ready.
Apollo Client provides two legacy SSR functions that are both replaced by prerenderStatic:
getDataFromTree - Executes all queries in a component tree and returns when data is ready. Uses renderToStaticMarkup by default.renderToStringWithData - Similar to getDataFromTree, but returns the rendered string using renderToString.These functions are deprecated. Use prerenderStatic instead, which offers better flexibility and performance with modern React rendering APIs.