メインコンテンツへスキップ

SvelteKit 1.0 以降のストリーミング、スナップショット、その他の新機能

SvelteKit の最新バージョンにおけるエキサイティングな改善点

Svelte チームは、SvelteKit 1.0 のリリース以来、懸命に作業を続けてきました。リリース以降に実装された主な新機能である、重要でないデータのストリーミングスナップショットルートレベルの設定について説明します。

load関数で重要でないデータをストリーミングする

SvelteKit は、load 関数を使用して、特定のルートのデータを取得します。ページ間を移動するとき、最初にデータをフェッチし、次にその結果でページをレンダリングします。ページの一部のデータのロードに他のデータよりも時間がかかる場合、特にそのデータが必須でない場合、これは問題になる可能性があります。すべてのデータの準備が整うまで、ユーザーは新しいページの一部も表示できません。

これを回避する方法はありました。特に、遅いデータをコンポーネント自体でフェッチすることで、最初に load からのデータでレンダリングし、次に遅いデータのフェッチを開始できました。しかし、これは理想的ではありませんでした。クライアントがレンダリングするまでフェッチを開始しないため、データはさらに遅延し、SvelteKit の load の規約も破る必要がありました。

SvelteKit 1.8 では、新しい解決策があります。サーバーの load 関数からネストされた Promise を返すことができ、SvelteKit はそれが解決する前にページのレンダリングを開始します。完了すると、その結果はページにストリーミングされます。

たとえば、次の load 関数を考えてみましょう

export const const load: PageServerLoadload: PageServerLoad = () => {
	return {
		post: anypost: fetchPost(),
		
streamed: {
    comments: any;
}
streamed
: {
comments: anycomments: fetchComments() } }; };

SvelteKit は、トップレベルにあるため、ページをレンダリングする前に fetchPost 呼び出しを自動的に待機します。ただし、ネストされた fetchComments 呼び出しの完了は待機しません。ページがレンダリングされ、data.streamed.comments はリクエストが完了すると解決される Promise になります。対応する +page.svelte では、Svelte のawait ブロックを使用してローディング状態を表示できます。

<script lang="ts">
	import type { PageData } from './$types';
	export let data: PageData;
</script>

<article>
	{data.post}
</article>

{#await data.streamed.comments}
	Loading...
{:then value}
	<ol>
		{#each value as comment}
			<li>{comment}</li>
		{/each}
	</ol>
{/await}

ここでは、プロパティ streamed に固有のものは何もありません。この動作をトリガーするために必要なのは、返されたオブジェクトのトップレベル以外の Promise だけです。

SvelteKit は、アプリのホスティングプラットフォームがストリーミングをサポートしている場合にのみ、レスポンスをストリーミングできます。一般的に、AWS Lambda (例: サーバーレス関数) を中心に構築されたプラットフォームはストリーミングをサポートしていませんが、従来の Node.js サーバーまたはエッジベースのランタイムはサポートしています。プロバイダーのドキュメントで確認してください。

プラットフォームがストリーミングをサポートしていない場合でも、データは利用できますが、レスポンスはバッファリングされ、すべてのデータのフェッチが完了するまでページのレンダリングは開始されません。

どのように機能するのか?

サーバーの load 関数からのデータをブラウザに送信するには、それをシリアライズする必要があります。SvelteKit は、devalue と呼ばれるライブラリを使用します。これは JSON.stringify に似ていますが、より優れています。JSON では処理できない値 (日付や正規表現など) を処理でき、自身を含む (またはデータ内に複数回存在する) オブジェクトをIDを壊さずにシリアライズでき、XSS 脆弱性から保護します。

ページをサーバー側でレンダリングするときに、devalue に Promise を deferred を作成する関数呼び出しとしてシリアライズするように指示します。これは、SvelteKit がページに追加するコードの簡略化されたバージョンです。

const const deferreds: Map<any, any>deferreds = new 
var Map: MapConstructor
new () => Map<any, any> (+3 overloads)
Map
();
var window: Window & typeof globalThiswindow.defer = (id) => { return new
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>

Creates 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: unknown) => voidfulfil, reject: (reason?: any) => voidreject) => {
const deferreds: Map<any, any>deferreds.Map<any, any>.set(key: any, value: any): Map<any, any>

Adds 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: unknown) => voidfulfil, reject: (reason?: any) => voidreject });
}); }; var window: Window & typeof globalThiswindow.resolve = (id, data, error) => { const const deferred: anydeferred = const deferreds: Map<any, any>deferreds.Map<any, any>.get(key: any): any

Returns 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: Map<any, any>deferreds.Map<any, any>.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: var window: Window & typeof globalThiswindow.defer(1) } };

このコードは、サーバー側でレンダリングされた HTML の残りの部分とともに、すぐにブラウザに送信されますが、接続は開いたままになります。後で、Promise が解決すると、SvelteKit は追加の HTML チャンクをブラウザにプッシュします。

<script>
	window.resolve(1, {
		data: [{ comment: 'First!' }]
	});
</script>

クライアント側のナビゲーションでは、わずかに異なるメカニズムを使用します。サーバーからのデータは改行区切り JSONとしてシリアライズされ、SvelteKit は devalue.parse を使用して、同様の deferred メカニズムを使用して値を再構築します。

// 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!"]

Promise はこの方法でネイティブにサポートされているため、load から返されるデータの任意の場所 (トップレベルを除く。これらは自動的に await されるため) に配置でき、devalue がサポートするあらゆるタイプのデータ (さらに多くの Promise を含む) で解決できます。

注意点: この機能には JavaScript が必要です。このため、エクスペリエンスの中核がすべてのユーザーに利用できるように、重要でないデータのみをストリーミングすることをお勧めします。

この機能の詳細については、ドキュメントを参照してください。sveltekit-on-the-edge.vercel.app (場所のデータは人工的に遅延されストリーミングされています) でデモを確認するか、Vercel で独自にデプロイしてください。Vercel では、Edge Functions と Serverless Functions の両方でストリーミングがサポートされています。

Qwik、Remix、Solid、Marko、React など、このアイデアの以前の実装からインスピレーションを得たことに感謝します。

スナップショット

以前の SvelteKit アプリでは、フォームへの入力を開始した後に移動すると、戻ってもフォームの状態は復元されませんでした。フォームはデフォルト値で再作成されます。状況によっては、これがユーザーにとって不満になる可能性があります。SvelteKit 1.5 以降、この問題を解決するための組み込みの方法であるスナップショットがあります。

これで、+page.svelte または +layout.svelte から snapshot オブジェクトをエクスポートできます。このオブジェクトには、capturerestore の 2 つのメソッドがあります。capture 関数は、ユーザーがページを離れるときに保存する状態を定義します。SvelteKit は、その状態を現在の履歴エントリに関連付けます。ユーザーがページに戻ると、以前に設定した状態で restore 関数が呼び出されます。

たとえば、textarea の値をキャプチャして復元する方法を次に示します。

<script lang="ts">
	import type { Snapshot } from './$types';

	let comment = '';

	export const snapshot: Snapshot = {
		capture: () => comment,
		restore: (value) => (comment = value)
	};
</script>

<form method="POST">
	<label for="comment">Comment</label>
	<textarea id="comment" bind:value={comment} />
	<button>Post comment</button>
</form>

フォーム入力の値やスクロール位置などが一般的な例ですが、スナップショットには任意の JSON シリアライズ可能なデータを保存できます。スナップショットデータはsessionStorageに保存されるため、ページがリロードされた場合や、ユーザーが別のサイトに移動した場合でも永続化されます。sessionStorage にあるため、サーバーサイドレンダリング中はアクセスできません。

詳細については、ドキュメントを参照してください。

ルートレベルのデプロイメント設定

SvelteKit は、プラットフォーム固有のアダプターを使用して、アプリのコードを本番環境へのデプロイ用に変換します。これまで、デプロイメントはアプリ全体レベルで設定する必要がありました。たとえば、アプリをエッジ関数またはサーバーレス関数としてデプロイすることはできましたが、両方同時にデプロイすることはできませんでした。これにより、アプリの一部にエッジを利用することが不可能になりました。いずれかのルートで Node API が必要な場合、その一部をエッジにデプロイすることはできませんでした。領域や割り当てられたメモリなど、デプロイメント設定の他の側面についても同様です。アプリ全体で各ルートに適用される 1 つの値を選択する必要がありました。

これで、+server.js+page(.server).js+layout(.server).js ファイルで config オブジェクトをエクスポートして、それらのルートのデプロイ方法を制御できます。+layout.js でこれを行うと、設定はすべての子ページに適用されます。config の型は、デプロイ先の環境に依存するため、アダプターごとに固有です。

import type { import ConfigConfig } from 'some-adapter';

export const const config: Configconfig: import ConfigConfig = {
	runtime: stringruntime: 'edge'
};

設定はトップレベルでマージされるため、ツリーの下位にあるページに対してレイアウトで設定された値をオーバーライドできます。詳細については、ドキュメントを参照してください。

Vercel にデプロイする場合は、SvelteKit とアダプターの最新バージョンをインストールすることで、この機能を利用できます。ルートレベルの設定をサポートするアダプターには SvelteKit 1.5 以降が必要なため、アダプターバージョンの大幅なアップグレードが必要になります。

npm i @sveltejs/kit@latest
npm i @sveltejs/adapter-auto@latest # or @sveltejs/adapter-vercel@latest

今のところ、Vercel アダプターのみがルート固有の設定を実装していますが、他のプラットフォームにこれを実装するための構成要素は揃っています。アダプターの作成者の方は、必要な内容については、PR の変更内容を参照してください。

Vercel でのインクリメンタル静的再生成

ルートレベルの設定により、別の要望の多かった機能も実現しました。Vercel にデプロイされた SvelteKit アプリで、インクリメンタル静的再生成 (ISR) を使用できるようになりました。ISR は、動的にレンダリングされたコンテンツの柔軟性を備えた、プリレンダリングされたコンテンツのパフォーマンスとコスト上の利点を提供します。

ISR をルートに追加するには、config オブジェクトに isr プロパティを含めます

export const 
const config: {
    isr: {};
}
config
= {
isr: {}isr: { // see Vercel adapter docs for the required options } };

その他多くの機能...

SvelteKit に貢献し、プロジェクトで使用してくださっている皆様に感謝いたします。以前にも申し上げましたが、Svelte はコミュニティプロジェクトであり、皆様からのフィードバックと貢献がなければ実現できなかったでしょう。