Streaming, snapshots, and other new features since SvelteKit 1.0

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

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 (/docs/kit/load#Streaming-with-promises), snapshots (/docs/kit/snapshots), and route-level config (/docs/kit/page-options#config).

Stream non-essential data in load functions (#Stream-non-essential-data-in-load-functions)SvelteKit uses load functions (/docs/kit/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:

export const const load: PageServerLoadload: PageServerLoad = () => { return { post: anypost: fetchPost(), streamed: { comments: any; }streamed: { comments: anycomments: 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 (/docs/svelte/await):

{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? (#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: const const deferreds: Mapdeferreds = new var Map: MapConstructor new () => Map (+3 overloads)Map(); module window var window: Window & typeof globalThisThe window property of a Window object points to the window object itself. MDN Reference (https://developer.mozilla.org/docs/Web/API/Window/window) window.defer = (id) => { return new var Promise: PromiseConstructor new (executor: (resolve: (value: any) => void, reject: (reason?: any) => void) => void) => PromiseCreates a new Promise. @paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.Promise((fulfil: (value: any) => voidfulfil, reject: (reason?: any) => voidreject) => { const deferreds: Mapdeferreds.Map.set(key: any, value: any): MapAdds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated. set(id: anyid, { fulfil: (value: any) => voidfulfil, reject: (reason?: any) => voidreject }); }); }; module window var window: Window & typeof globalThisThe window property of a Window object points to the window object itself. MDN Reference (https://developer.mozilla.org/docs/Web/API/Window/window) window.resolve = (id, data, error) => { const const deferred: anydeferred = const deferreds: Mapdeferreds.Map.get(key: any): anyReturns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map. @returnsReturns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.get(id: anyid); const deferreds: Mapdeferreds.Map.delete(key: any): boolean@returnstrue if an element in the Map existed and has been removed, or false if the element does not exist.delete(id: anyid); if (error: anyerror) { const deferred: anydeferred.reject(error: anyerror); } else { const deferred: anydeferred.fulfil(data: anydata); } }; // devalue converts your data into a JavaScript expression const const data: { post: { title: string; content: string; }; streamed: { comments: any; }; }data = { post: { title: string; content: string; }post: { title: stringtitle: 'My cool blog post', content: stringcontent: '...' }, streamed: { comments: any; }streamed: { comments: anycomments: module window var window: Window & typeof globalThisThe window property of a Window object points to the window object itself. MDN Reference (https://developer.mozilla.org/docs/Web/API/Window/window) 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: 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: // 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 (/docs/kit/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 (#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:
No comments yet.