diff --git a/documentation/blog/2023-02-21-streaming-snapshots-sveltekit.md b/documentation/blog/2023-02-21-streaming-snapshots-sveltekit.md new file mode 100644 index 0000000000..a030e8fc41 --- /dev/null +++ b/documentation/blog/2023-02-21-streaming-snapshots-sveltekit.md @@ -0,0 +1,207 @@ +--- +title: Streaming, snapshots, and other new features since SvelteKit 1.0 +description: Exciting improvements in the latest version of SvelteKit +author: Geoff Rich +authorURL: https://geoffrich.net +--- + +The Svelte team has been hard at work since the release of SvelteKit 1.0. Let’s talk about some of the major new features that have shipped since launch: [streaming non-essential data](https://kit.svelte.dev/docs/load#streaming-with-promises), [snapshots](https://kit.svelte.dev/docs/snapshots), and [route-level config](https://kit.svelte.dev/docs/page-options#config). + +## Stream non-essential data in load functions + +SvelteKit uses [load functions](https://kit.svelte.dev/docs/load) to retrieve data for a given route. When navigating between pages, it first fetches the data, and then renders the page with the result. This could be a problem if some of the data for the page takes longer to load than others, especially if the data isn’t essential – the user won’t see any part of the new page until all the data is ready. + +There were ways to work around this. In particular, you could fetch the slow data in the component itself, so it first renders with the data from `load` and then starts fetching the slow data. But this was not ideal: the data is even more delayed since you don’t start fetching until the client renders, and you’re also having to break SvelteKit’s `load` convention. + +Now, in SvelteKit 1.8, we have a new solution: you can return a nested promise from a server load function, and SvelteKit will start rendering the page before it resolves. Once it completes, the result will be [streamed](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) to the page. + +For example, consider the following `load` function: + +```ts +// @errors: 2304 +export const load: PageServerLoad = () => { + return { + post: fetchPost(), + streamed: { + comments: fetchComments() + } + }; +}; +``` + +SvelteKit will automatically await the `fetchPost` call before it starts rendering the page, since it’s at the top level. However, it won’t wait for the nested `fetchComments` call to complete – the page will render and `data.streamed.comments` will be a promise that will resolve as the request completes. We can show a loading state in the corresponding `+page.svelte` using Svelte’s [await block](https://svelte.dev/docs#template-syntax-await): + +```svelte + + +
+ {data.post} +
+ +{#await data.streamed.comments} + Loading... +{:then value} +
    + {#each value as comment} +
  1. {comment}
  2. + {/each} +
+{/await} +``` + +There is nothing unique about the property `streamed` here – all that is needed to trigger the behavior is a promise outside the top level of the returned object. + +SvelteKit will only be able to stream responses if your app’s hosting platform supports it. In general, any platform built around AWS Lambda (e.g. serverless functions) will not support streaming, but any traditional Node.js server or edge-based runtime will. Check your provider’s documentation for confirmation. + +If your platform does not support streaming, the data will still be available, but the response will be buffered and the page won’t start rendering until all data has been fetched. + +## How does it work? + +In order for data from a server `load` function to get to the browser, we have to _serialize_ it. SvelteKit uses a library called [devalue](https://github.com/Rich-Harris/devalue), which is like `JSON.stringify` but better — it can handle values that JSON can't (like dates and regular expressions), it can serialize objects that contain themselves (or that exist multiple times in the data) without breaking identity, and it protects you against [XSS vulnerabilities](https://github.com/rich-harris/devalue#xss-mitigation). + +When we server-render a page, we tell devalue to serialize promises as function calls that create a _deferred_. This is a simplified version of the code SvelteKit adds to the page: + +```js +// @errors: 2339 7006 +const deferreds = new Map(); + +window.defer = (id) => { + return new Promise((fulfil, reject) => { + deferreds.set(id, { fulfil, reject }); + }); +}; + +window.resolve = (id, data, error) => { + const deferred = deferreds.get(id); + deferreds.delete(id); + + if (error) { + deferred.reject(error); + } else { + deferred.fulfil(data); + } +}; + +// devalue converts your data into a JavaScript expression +const data = { + post: { + title: 'My cool blog post', + content: '...' + }, + streamed: { + comments: window.defer(1) + } +}; +``` + +This code, along with the rest of the server-rendered HTML, is sent to the browser immediately, but the connection is kept open. Later, when the promise resolves, SvelteKit pushes an additional chunk of HTML to the browser: + +```html + +``` + +For client-side navigation, we use a slightly different mechanism. Data from the server is serialized as [newline delimited JSON](https://dataprotocols.org/ndjson/), and SvelteKit reconstructs the values — using a similar deferred mechanism — with `devalue.parse`: + +```json +// this is generated immediately — note the ["Promise",1]... +[{"post":1,"streamed":4},{"title":2,"content":3},"My cool blog post","...",{"comments":5},["Promise",6],1] + +// ...then this chunk is sent to the browser once the promise resolves +[{"id":1,"data":2},1,[3],{"comment":4},"First!"] +``` + +Because promises are natively supported in this way, you can put them anywhere in the data returned from `load` (except at the top level, since we automatically await those for you), and they can resolve with any type of data that devalue supports — including more promises! + +One caveat: this feature needs JavaScript. Because of this, we recommend that you only stream in non-essential data so that the core of the experience is available to all users. + +For more on this feature, see [the documentation](https://kit.svelte.dev/docs/load#streaming-with-promises). You can see a demo at [sveltekit-on-the-edge.vercel.app](https://sveltekit-on-the-edge.vercel.app/edge) (the location data is artificially delayed and streamed in) or [deploy your own on Vercel](https://vercel.com/templates/svelte/sveltekit-edge-functions), where streaming is supported in both Edge Functions and Serverless Functions. + +We're grateful for the inspiration from prior implementations of this idea including Qwik, Remix, Solid, Marko, React and many others. + +## Snapshots + +Previously in a SvelteKit app, if you navigated away after starting to fill out a form, going back wouldn’t restore your form state – the form would be recreated with its default values. Depending on the context, this can be frustrating for users. Since SvelteKit 1.5, we have a built-in way to address this: snapshots. + +Now, you can export a `snapshot` object from a `+page.svelte` or `+layout.svelte`. This object has two methods: `capture` and `restore`. The `capture` function defines what state you want to store when the user leaves the page. SvelteKit will then associate that state with the current history entry. If the user navigates back to the page, the `restore` function will be called with the state you previously had set. + +For example, here is how you would capture and restore the value of a textarea: + +```svelte + + +
+ +