React
Server vs universal load in SvelteKit: who runs where
In SvelteKit +page.server.ts runs load on the server only, +page.ts also in the browser: the choice depends on what the data touches and how often it runs.
Key points
- The file with the .server suffix stays out of the browser bundle: database and private variables belong there.
- Universal load runs on the server during the render and again in the browser on each navigation.
- Whatever load returns is serialised: classes and functions do not cross the boundary.
In SvelteKit +page.server.ts runs load on the server only, while +page.ts runs the same code on the server during the render and in the browser on every navigation: the choice depends on what the data touches.
Two files, two contracts
The .server suffix keeps the module out of the browser bundle, so you can import the database and read private environment variables there. The universal file ends up in the browser, so no secrets and no driver.
npx sv check
// src/routes/orders/+page.server.ts
import { db } from "$lib/server/db";
export const load = async () => {
const orders = await db.order.findMany({ take: 20 });
return { orders };
};
In the universal file you get fetch, params, url and depends: the fetch is SvelteKit's, which forwards cookies and deduplicates requests during the render.
// src/routes/products/+page.ts
export const load = async ({ fetch, depends }) => {
depends("app:products");
const res = await fetch("/api/products");
return { products: await res.json() };
};
The data has to cross the boundary
Whatever load returns is serialised: numbers, strings, arrays, Date and Map cross, a class instance or a database connection does not. The error appears in the build or at first render, not where you look for it.
How to choose
| File | Where it runs | What it may use |
|---|---|---|
| +page.server.ts | On the server only | Database, secrets, request cookies |
| +page.ts | Server in SSR, browser on navigation | fetch, public APIs, shared modules |
| +layout.server.ts | On the server only | Data shared by a group of pages |
| +page.ts with a relative fetch | Both, the same code | Public endpoints already exposed |
The limitation to plan for
Universal load runs twice: on the server for the initial HTML and in the browser when you navigate. A slow fetch there costs twice, and a server load that returns a lot writes all of it into the HTML and the client cache.
Mistakes you will see
- Importing a server module from
+page.ts: the build stops, because that code cannot reach the browser. - Returning a class or an object with methods: serialisation drops the methods and the component gets mute data.
- Reading
document.cookiein+page.ts: it does not exist during server rendering, so the first HTML differs from the second. - Repeating in
+page.server.tsa fetch the component already makes: two requests for the same data.
In the catalogue the data arrives in ready-made tables: table collects the components, the table block brings the rows, and svelte-bits installs as source for the rest of the page. Form handling is in SvelteKit form actions, route files in routing and layouts in SvelteKit, sessions in authentication hooks, the comparison in Svelte vs React.
Frequently asked questions
Can I use both in the same route?
Yes, but only one of them returns data to the page: when both exist, universal load receives the server load result in data and cannot overwrite it. Keep the server for data and the universal one for browser-only work.
Why is the fetch inside universal load better than the global one?
Because the one SvelteKit provides forwards cookies during server rendering and deduplicates identical requests. The global fetch knows nothing about the current request and cannot do either.
Does server load run on client navigations?
Yes. On the client SvelteKit calls an internal endpoint generated for the route, so the code stays on the server but the result still arrives. That is why a slow query still hurts after the first load.
Where does shared code go?
In $lib. If it touches the database or secrets it goes in $lib/server, which the build refuses to import from a module meant for the browser. Everything else can sit in $lib without a suffix.
Why do I get a serialisation error?
Because the returned value holds something that does not survive the trip, such as a function, a class or an unhandled BigInt. Return plain data and rebuild the object in the component.