You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/documentation/docs/06-runtime/05-warp.md

237 lines
9.3 KiB

---
title: Warp
---
In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes:
```svelte
<script>
import { getUser } from 'my-database-library';
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
</script>
<h1>{user.name}</h1>
```
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `Warp` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
A `Warp` is a [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) whose contents are sent from the server to the client. Anything you add to it while rendering on the server is serialized into the `head` returned from `render`, and is available in the `Warp` with the same id on the client. To fix the example above:
```svelte
<script>
import { Warp } from 'svelte';
import { getUser } from 'my-database-library';
const warp = new Warp('my-app');
// On the server, this calls `getUser` and stores the result. On the client,
// the result from the server is already there, so `getUser` isn't called.
const user = await warp.getOrInsertComputed('user', () => getUser());
</script>
<h1>{user.name}</h1>
```
This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration:
```ts
import { Warp } from 'svelte';
const warp = new Warp<string, number>('my-app');
const rand = warp.getOrInsertComputed('random', () => Math.random());
```
On the client, a `Warp` behaves like any other `Map` — values you add to it stay there until you remove them with `delete` or `clear`. On the server, each key can only be set once, and values can't be removed, since they may already be in use or on their way to the client. For the same reason, treat values as immutable once they've been added to a `Warp`.
Keys can be strings, numbers, booleans or bigints. If you're a library author, use your package name as the `Warp`'s id, so that it doesn't conflict with other libraries:
```js
import { Warp } from 'svelte';
const warp = new Warp('my-datafetching-library');
```
## Serialization
All data added to a `Warp` must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. You can also fearlessly use promises:
```svelte
<script>
import { Warp } from 'svelte';
const warp = new Warp('my-app');
const promises = warp.getOrInsertComputed('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2)
}
});
</script>
{await promises.one}
{await promises.two}
```
If a promise rejects, the promise on the client will reject too. The rejection reason is passed through the [`transformError`](svelte-server#render) option of `render` first, so that sensitive information doesn't leak to the client — if you don't provide `transformError`, the client receives a generic error instead.
To serialize other kinds of values, pass a `replacer` to `render`. It works like the replacer for devalue's [`uneval`](https://github.com/sveltejs/devalue#custom-types), receiving each value and a `js` tag that you can use to return the JavaScript that recreates it on the client:
```js
/// file: server.js
// @noErrors
import { render } from 'svelte/server';
import App from './App.svelte';
import { Vector } from './vector.js';
// ---cut---
const { head, body } = await render(App, {
replacer: (value, js) => {
if (value instanceof Vector) {
return js`new Vector(${value.x}, ${value.y})`;
}
}
});
```
## Using `Warp` outside components
`Warp` can only be used on the server while rendering. If you need to add values before the render starts — for example, while loading data for a page — wrap the work in `withWarp`. A `render` inside `withWarp` will serialize everything added to `Warp` instances inside it:
```js
/// file: server.js
// @noErrors
import { render, withWarp } from 'svelte/server';
import { Warp } from 'svelte';
import App from './App.svelte';
import { getUser } from 'my-database-library';
// ---cut---
const warp = new Warp('my-app');
const { head, body } = await withWarp(async () => {
const user = await warp.getOrInsertComputed('user', () => getUser());
return render(App, { props: { user } });
});
```
`withWarp` also accepts a `replacer`, which is used for any values the `replacer` passed to `render` doesn't handle. Each `withWarp` can only contain one `render`.
## Streaming
By default, when the server encounters a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet, it renders the `pending` snippet and nothing else. The client then renders the boundary's contents itself, which means any data they need only starts loading once the page has hydrated.
With the `experimental.streaming` compiler option, the server also starts rendering the boundary's contents in the background (discarding the output). Any values they add to a `Warp` are sent to the client as soon as they're available, so the client can use them instead of loading the data again:
```js
/// file: svelte.config.js
export default {
compilerOptions: {
experimental: {
async: true,
streaming: true
}
}
};
```
Values that have already resolved by the time the HTML is rendered are included in the `head`, as usual. The rest are streamed via the `tail` of the render result, which contains `<script>` tags that you **must** write into the response after the HTML — otherwise, promises on the client that are waiting for data from the server will never settle:
```js
/// file: server.js
// @noErrors
import { render } from 'svelte/server';
import App from './App.svelte';
// ---cut---
const { head, body, tail } = await render(App);
response.write(`<html><head>${head}</head><body>${body}`);
for await (const script of tail) {
response.write(script);
}
response.end('</body></html>');
```
If you stop iterating over `tail` early, the background work is stopped. Since the streamed `<script>` tags can't be known in advance, streaming can't be used with `csp: { hash: true }` — use a `nonce` instead.
You can still change a `Warp` on the client while values are streaming in. Your changes take precedence over the values from the server, including ones that arrive later — for example, after `warp.clear()`, values streamed in afterwards won't be visible. This means that a component which is still waiting for data from the server will load it again on the client instead.
## CSP
`Warp` adds an inline `<script>` block to the `head` returned from `render`. If you're using [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) (CSP), this script will likely fail to run. You can provide a `nonce` to `render`:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
// ---cut---
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
csp: { nonce }
});
```
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
```js
/// file: server.js
let response = new Response();
let nonce = 'xyz123';
// ---cut---
response.headers.set(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
```
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
If instead you are generating static HTML ahead of time, you must use hashes instead:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
// ---cut---
const { head, body, hashes } = await render(App, {
csp: { hash: true }
});
```
`hashes.script` will be an array of strings like `["sha256-abcd123"]`. As with `nonce`, the hashes should be used in your CSP header:
```js
/// file: server.js
let response = new Response();
let hashes = { script: ['sha256-xyz123'] };
// ---cut---
response.headers.set(
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
);
```
We recommend using `nonce` over hash if you can, as `hash` cannot be used with [streaming](#Streaming).
## `hydratable`
`hydratable` is the predecessor of `Warp`, and is deprecated. `hydratable(key, fn)` behaves like `warp.getOrInsertComputed(key, fn)`, except that on the client, it only uses the value from the server during hydration:
```js
// @noErrors
import { hydratable, Warp } from 'svelte';
// before
const user = await hydratable('user', () => getUser());
// after
const warp = new Warp('my-app');
const user = await warp.getOrInsertComputed('user', () => getUser());
```