A SvelteKit server load function runs for a request and can access server-only identity and data services. It returns serializable data to the page; that result enters HTML or client navigation payloads, so it must contain only information the viewer may receive. A long-running server process also holds module variables between requests. A mutable module-level current reviewer or case list is therefore unsafe for user-specific state. Component instances receive page data through props. Server-rendered markup and the browser's first render need compatible structure and initial values for hydration. Browser storage, viewport size, or local time can legitimately change the later view, but reading them during the first render can produce a mismatch and discard useful server work.
SvelteKit Request-Scoped Load and Hydration State
Working case
Reviewer 47 opens a permit queue whose server load returns 47 summaries. A module-level array retains that list. Reviewer 81 arrives on the same worker and sees one of 47's private case titles in the response. Separately, the browser reads a saved 29-row limit before hydration while the server sent 47 rows, so the initial trees disagree. The repair puts permission lookup and list retrieval inside each request's load function, returns only that viewer's allowed summaries, and leaves local display preferences until after the browser has taken over. The first browser view uses the serialized authorized batch and deterministic formatting; a later preference update may narrow it.
Implementation boundary
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
const reviewer = locals.reviewer;
if (!reviewer) error(401, 'Sign in required');
const firstPage = await locals.permits.listAuthorized({
reviewerId: reviewer.id, limit: 47
});
return {
caseSummaries: firstPage.cases.map(permitCase => ({
id: permitCase.id, title: permitCase.title, status: permitCase.status
})),
nextCursor: firstPage.nextCursor
};
};Read identity from the request's server context, enforce the permission boundary before fetching summaries, and return only the fields the page needs. Do not write user data to a module-level let, singleton cache keyed only by URL, or shared reactive store. Treat a load result as a response payload, even if a property feels internal. Keep date and number formatting stable between server and first browser render or pass preformatted display strings with a clear locale contract. On the page, take data from props and initialize source state from it. Apply local storage after mount and handle storage denial. If a background refresh arrives, compare account and request generation before replacing private data. Run two simultaneous requests through one worker, inspect both HTML and data payloads, then check the first browser tree under different local settings.
Cost and boundaries
Request-scoped fetch and authorization use server CPU and database work per viewer. A shared cache can reduce that cost only when its key and invalidation model include the full visibility boundary; a URL-only key cannot represent reviewer permissions. Serializing 47,000 cases increases HTML or navigation bytes, so return a bounded first page and a cursor for more. Hydration can save DOM reconstruction, but a browser-only branch that changes initial markup may force recovery and shift focus. A post-mount preference update can also move rows; keep the change predictable and test it with keyboard and assistive technology. Measure response size, duplicate fetches, hydration warnings, and cross-user isolation together.
Failure trace
Render reviewers 47 and 81 concurrently on one process and scan each response for the other's private IDs. Change the browser's stored limit to 29 and time zone to another region; the first render must still match the server's 47-row response, with personalization after mount. Disable JavaScript and confirm the authorized server page remains readable. Simulate an expired session during a client refresh, then verify old data disappears rather than being copied into a new account's state. Poison a shared URL cache in a test and assert permission prevents its use. Delay a previous account's fetch until after sign-out and reject its result.
Verification
- Concurrent responses contain only each reviewer's permitted case IDs.
- The first browser tree matches the server's serialized page.
- Account changes reject late private responses and clear local drafts.
Practice drill
Build a request-scoped load for a permit queue with two reviewers whose permitted case sets do not overlap. Return a bounded first page, count, and cursor, then render it with deterministic labels. Send concurrent requests through one server worker and compare their serialized payloads. On the client, set a browser-only row limit after mount and record the initial DOM and later personalized DOM separately. Test storage denial, different time zones, sign-out during refresh, and a late previous-account response. Decide whether any cache can be shared; document the identity and permission key it would require.
Decision note
Keep authorization and private data inside each request; hydrate from the same permitted payload before applying browser preferences.
Common Mistakes
- Keeping a mutable current reviewer in module scope.
- Sending private service objects in a load result.
- Reading browser storage before the first hydrated render.
Related lessons
Svelte Reactivity and Server Boundaries; Svelte Runes: Source State, Derived Views, and Effect Cleanup; Svelte Props, Callbacks, and Keyed Editor Ownership; SvelteKit Form Actions, Validation, and Mutation Replay; Server Rendering and Client Data Flow; Angular SSR Hydration and Private Transfer State.
Apply and check
Build Project: Svelte permit review and approval action and review Web Development: Svelte reactivity and server boundaries quiz.
