kit
kit copied to clipboard
Teleport data from server to client in universal `load` functions
Describe the problem
One point of confusion with load functions is that universal loads run during SSR and upon hydration (and then for subsequent client-side navigations). This is most visible if you return something non-deterministic:
// src/routes/+page.js
export function load() {
return {
random: Math.random()
};
}
The value of data.random inside +page.svelte will differ between the server-rendered HTML and the hydrated document. If it were a +page.server.js instead, the result of calling the load function would be serialized, and would therefore be consistent.
There are good reasons for re-running universal load functions upon hydration:
- It allows you to return different data between server and browser, in cases where that's desirable
- It means we don't need to serialize the output, which is often larger than the input (for example, on a project at the NYT we were doing statistical analysis of some JSON data. The input was large, in the 100s of kbs, but the output was huge — megabytes of moving averages and so on)
- We can return non-serializable objects such as components and stores, enabling advanced whizzbangery
(Note that event.fetch calls are not repeated — the responses are serialized into, and read from, the HTML.)
Despite that, it would be useful to have a mechanism to avoid re-running code in cases where you just want to use the same value between server and client, and are happy with the serialization constraints.
Describe the proposed solution
I propose a new event.teleport helper:
export function load({ teleport }) {
const random = teleport(() => Math.random());
return {
random
};
}
- During SSR, the result of calling the callback would be serialized (using the same mechanisms we use for server load functions) and assigned to an automatically-generated ID
- During hydration,
teleport(...)would just return the data associated with the automatically-generated ID - During client-side navigation,
teleportwould just befn => fn()
Automatically generating IDs requires that teleport be called synchronously inside the load body (and not be inside if blocks etc — in other words, hooks rules). In some circumstances that might be untenable, so users could specify a key (if a key is reused, we would throw an error):
// either this...
const random = teleport('random', () => Math.random());
// ...or this:
const random = teleport(() => Math.random(), 'random');
Bikeshedding alert
If the key is optional, having it be the second argument would be more logical. But having it be the first argument would result in neater code:
// prettier prefers string-first-function-second...
const object = teleport('object', () => ({
answer: 42
}));
// ...to string-second-function-first:
const object = teleport(
() => ({
answer: 42
}),
'object'
)
Reusing entire load functions
If you wanted to, you could easily reuse the entire function body:
export const load = (event) => {
return event.teleport(() => ({
stuff: get_stuff(event.params.stuff),
more_stuff: get_more_stuff(event.params.stuff),
}));
};
Promises
We could use the same promise serialization mechanism we currently use:
export async function load({ teleport }) {
const randomized = await teleport(async () => {
const response = await fetch('https://api.example.com/things');
const { things } = await response.json();
return randomize(things);
});
return {
randomized
};
}
Alternatives considered
- different name —
keep,sticky,reuseetc - key-first, key-second, key-optional
Importance
nice to have
Additional Information
No response
Explicit true for generated key?
const object = teleport(true, () => ({ answer: 42 }))
that would defeat the object, really. if you're going to add true you may as well just add a key
I like keeping the ID as a second param (always)
Can you elaborate on "We could use the same promise serialization mechanism we currently use:"?
From the example snippets, I'd think teleport simply acts like
function teleport(input) {
if (input_is_promise)
return input.then((result) => {
serialize_somehow(result);
return result;
})
else {
serialize_somehow(result);
return result;
}
}
PS, if someone's looking for more name suggestions https://github.com/sveltejs/kit/issues/3729 👀
input would never be a promise, it would only ever be a function that returns a promise
Can you elaborate
Yeah, it would basically use the same logic we use for serializing data from a server load function: https://github.com/sveltejs/kit/blob/40e85888d7cf53fb4c92bdca9efaa79c1fe5c682/packages/kit/src/runtime/server/page/render.js#L489-L553
On the server it would be something like this:
const teleported = [];
const streamed = [];
function teleport(fn, id = uid++) {
const result = fn();
// this calls `devalue.uneval` with a replacer that deals with Promises
const { data, chunks } = serialize(result);
// `data` contains `__sveltekit_xyz123.defer(someid)` etc
teleported.push(`__sveltekit_xyz123.teleported.set(${id}, ${data})`);
// `chunks` is an `AsyncIterable<string>` where each string contains
// `__sveltekit_xyz123.resolve(someid, somedata)`
if (chunks) streamed.push(chunks);
return result;
}
On the client it would be this:
// during hydration
function teleport(fn, id = uid++) {
return __sveltekit_xyz123.teleported.get(id);
}
// during navigation
function teleport(fn, id) {
return fn();
}
We could have two overloads for teleport, to have both nice formatting and optional key:
declare function teleport<T>(data: () => T): T
declare function teleport<T>(key: string, data: () => T): T
The implementation would then check which type is the first argument.
Need this badly
Any kind of workarounds? It's slowing our app when we need to do the exact same request multiple times despite the data having not changed
This would improve app mounting speeds significantly since it no longer needs to reload data loaded during SSR.
The overload which @tmaxmax suggested seems to be ideal, although I wonder what will happen if teleports are called in different orders from server/client side (eg: if blocks or etc) without an explicit key parameter?
Does this relate in any way to Svelte 5 rune syntax?
Use case – JSON-RPC like API with tracing
We hit our API by batching multiple RPC requests in a single HTTP request. The body of the HTTP request is the array of RPC request bodies, which are JSON objects. For tracing, each RPC request is assigned separate trace ID and span ID.
This is incompatible with SvelteKit fetch caching in SSR. On the server, SvelteKit custom fetch caches responses in the HTML; during hydration, fetch retrieves the responses from the HTML cache.
The problem is that the cache key is an hash of the request header and body. The HTTP request body includes each RPC's trace ID and span ID, which differ between SSR and hydration, resulting in cache misses.
For context, we are using @effect/rpc but I think this applies more generally.