Skip to main content

Streaming、snapshots、その他 SvelteKit 1.0 以降の新機能

SvelteKit 最新バージョンのエキサイティングな改善

翻訳 : Svelte 日本コミュニティ
原文 : https://svelte.dev/blog/streaming-snapshots-sveltekit

日本語版は原文をよりよく理解するための参考となることを目的としています。
正確な内容については svelte.dev の原文を参照してください。
日本語訳に誤解を招く内容がある場合は下記のいずれかからお知らせください。

Svelte チームは SvelteKit 1.0 がリリースされたあとも懸命に取り組んできました。ローンチ後にリリースされたいくつかのメジャーな新機能についてご紹介します: streaming non-essential datasnapshots、そして route-level config です。

Stream non-essential data in load functions

SvelteKit は load 関数を使用してルート(route)のデータを取得します。ページ間で移動する場合、まず最初にデータを取得し、それからその結果を用いてページをレンダリングします。このため、もしデータの一部が他のデータよりも取得に時間がかかる場合、特にそのデータが重要ではない場合、問題になるでしょう – すべてのデータが揃わないと、ユーザーは新しいページのどの部分も見ることができないからです。

これを回避する方法もありました。具体的には、コンポーネント自体で遅いデータを取得することができるので、まず load で取得したデータでレンダリングし、そのあとで遅いデータの取得を開始します。しかしこれは理想的ではありませんでした: クライアントがレンダリングするまでデータの取得を開始しないため、データはさらに遅延しますし、SvelteKit の load の規約を破ることにもなります。

今回、SvelteKit 1.8 において、新たなソリューションを提供します: server load 関数からネストした promise を返すと、SvelteKit はそれが解決する前にページのレンダリングを開始します。ネストした promise は完了し次第、その結果がページにストリーミングされます。

例えば、次の load 関数について考えてみましょう:

ts
export const load: PageServerLoad = () => {
Cannot find name 'PageServerLoad'.2304Cannot find name 'PageServerLoad'.
return {
post: fetchPost(),
Cannot find name 'fetchPost'.2304Cannot find name 'fetchPost'.
streamed: {
comments: fetchComments()
Cannot find name 'fetchComments'.2304Cannot find name 'fetchComments'.
}
};
};

SvelteKit は自動的にこの fetchPost の呼び出しを await してからページのレンダリングを開始します。なぜならそれがトップレベルだからです。しかし、ネストした fetchComments の呼び出しが完了するのは待ちません – ページはレンダリングされ、data.streamed.comments はリクエストが完了すると解決する promise となります。+page.svelte で、Svelte の await block を使用してロード中の状態を表示することもできます:

<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 を中心に構築されたプラットフォーム (例えば serverless functions) はストリーミングをサポートしていませんが、従来の Node.js サーバーや edge ベースのランタイムはサポートしています。プロバイダーのドキュメントで確認してみてください。

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

How does it work?

データを server load 関数からブラウザに送信するには、データを シリアライズ する必要があります。SvelteKit は devalue というライブラリを使用しています。これは JSON.stringify のようなものですが、より優れています — JSON では扱うことができない値 (例えば date や正規表現など) も扱うことができ、自身をその中に含むような (またはそのデータの中に何度も現れるような) オブジェクトもそのアイデンティティを破壊することなくシリアライズすることができ、そして XSS 脆弱性 から保護することができます。

ページをサーバーでレンダリングする際、promise を、deferred(遅延) を作成する function call としてシリアライズするよう devalue に指示します。これは SvelteKit がページに追加するコードを簡略化したものです。

ts
const deferreds = new Map();
window.defer = (id) => {
Property 'defer' does not exist on type 'Window & typeof globalThis'.
Parameter 'id' implicitly has an 'any' type.
2339
7006
Property 'defer' does not exist on type 'Window & typeof globalThis'.
Parameter 'id' implicitly has an 'any' type.
return new Promise((fulfil, reject) => {
deferreds.set(id, { fulfil, reject });
});
};
window.resolve = (id, data, error) => {
Property 'resolve' does not exist on type 'Window & typeof globalThis'.
Parameter 'id' implicitly has an 'any' type.
Parameter 'data' implicitly has an 'any' type.
Parameter 'error' implicitly has an 'any' type.
2339
7006
7006
7006
Property 'resolve' does not exist on type 'Window & typeof globalThis'.
Parameter 'id' implicitly has an 'any' type.
Parameter 'data' implicitly has an 'any' type.
Parameter 'error' implicitly has an 'any' type.
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)
Property 'defer' does not exist on type 'Window & typeof globalThis'.2339Property 'defer' does not exist on type 'Window & typeof globalThis'.
}
};

このコードは、サーバーレンダリングされた一部の HTML と一緒にブラウザにすぐに送信されますが、コネクションは開いたままになっています。その後、promise が解決すると、SvelteKit は追加の HTML チャンクをブラウザにプッシュします:

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

クライアントサイドナビゲーションの場合は、少し異なるメカニズムを使用します。サーバーからのデータは newline delimited JSON としてシリアライズされ、SvelteKit は devalue.parse で似たような遅延メカニズムを使用してその値を再構築します:

ts
// 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 などの、このアイデアの先行実装からインスピレーションを受けました。深く感謝します。

Snapshots

以前までの SvelteKit アプリでは、フォームに入力を開始したあとで移動して、そのあと戻ってくるとフォームの state は復元されず、デフォルトの値でフォームが再作成されていました。場合によっては、ユーザーはフラストレーションが溜まるかもしれません。SvelteKit 1.5 以降は、これに対応するための方法が組み込まれています: それが snapshots です。

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

こちらは textarea の値を capture し、restore する方法の例です:

<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>

フォームの input の値やスクロールポジションなどは一般的な例で、JSON-serializable なデータならなんでも snapshot に保存することができます。snapshot のデータは sessionStorage に保存されるので、ページがリロードされたときや、ユーザーがまったく別のサイトに移動したときにも保持されます。sessionStorage に保存されるため、サーバーサイドレンダリング中にアクセスすることはできません。

詳細は、ドキュメントをご覧ください。

Route-level deployment configuration

SvelteKit はプラットフォームごとに固有の adapter を使用してプロダクションへのデプロイ用にアプリのコードを変換しています。これまでは、デプロイメントの設定をアプリ全体レベルで行わなければなりませんでした。例えば、アプリを edge function としてデプロイするか、serverless function としてデプロイするか、どちらか一方は可能でしたが、両方同時に行うことはできませんでした。これでは、アプリの一部だけを edge にするというメリットを得ることができません – もし Node API を必要とするルート(route)がある場合、アプリ全体を edge にデプロイすることができないのです。リージョンの選択やメモリ割り当てなど、デプロイ設定の他の側面についても同様です: アプリ全体、すべてのルート(route)に適用される1つの値を選択しなければならなかったのです。

そしてこの度、config オブジェクトを +server.js+page(.server).js+layout(.server).js ファイルでエクスポートすることができるようになり、これらのルート(route)をどうやってデプロイするかコントロールできるようになりました。+layout.js でこれを行うと、そのすべての子ページに設定が適用されます。config の型は、デプロイ先の環境に依存するため、各 adapter ごとにユニークです。

ts
import type { Config } from 'some-adapter';
Cannot find module 'some-adapter' or its corresponding type declarations.2307Cannot find module 'some-adapter' or its corresponding type declarations.
export const config: Config = {
runtime: 'edge'
};

Config はトップレベルでマージされるため、レイアウトで設定された値をツリーのさらに下のページで上書きすることができます。詳細はドキュメントをご覧ください。

Vercel にデプロイする場合、最新バージョンの SvelteKit と adapter をインストールすることでこの機能のメリットを享受することができます。ルートレベル(route-level)の config をサポートする adapter は SvelteKit 1.5 以降が必要であるため、adapter のバージョンを大幅にアップグレードする必要があるかもしれません。

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

今現在は、Vercel adapter のみがルート固有(route-specific)の config を実装していますが、他のプラットフォーム向けでもこれを実装するためのビルディングブロックがあります。もしあなたが adapter の作者なら、この PR の変更点を参照し、要求事項を確認してください。

Incremental static regeneration on Vercel

ルートレベル(Route-level)の config では、もう1つ要望の多かった機能も使えるようになりました – Vercel にデプロイされる SvelteKit アプリで、incremental static regeneration (ISR) が使用できるようになりました。ISR は、プリレンダリングされたコンテンツにおけるコストとパフォーマンスの優位性と、動的なレンダリングコンテンツの柔軟性の両方を提供します。

ISR をルート(route)に追加するには、config オブジェクトに isr プロパティを追加します:

ts
export const config = {
isr: {
// 必須のオプションについては Vercel adapter のドキュメントをご覧ください
}
};

And much more...

SvelteKit にコントリビュートしてくれた皆様、SvelteKit をプロジェクトで使ってくださっている皆様、ありがとうございます。以前にもお伝えしましたが、Svelte はコミュニティプロジェクトであり、皆様のフィードバックやコントリビューションがなくては成り立たないものです。