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: PageServerLoad
load: PageServerLoad = () => {
return {
post: any
post: fetchPost(),
streamed: {
comments: any;
}
streamed: {
comments: any
comments: 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 globalThis
window.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.
Promise((fulfil: (value: unknown) => void
fulfil, reject: (reason?: any) => void
reject) => {
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: any
id, { fulfil: (value: unknown) => void
fulfil, reject: (reason?: any) => void
reject });
});
};
var window: Window & typeof globalThis
window.resolve = (id, data, error) => {
const const deferred: any
deferred = 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.
get(id: any
id);
const deferreds: Map<any, any>
deferreds.Map<any, any>.delete(key: any): boolean
delete(id: any
id);
if (error: any
error) {
const deferred: any
deferred.reject(error: any
error);
} else {
const deferred: any
deferred.fulfil(data: any
data);
}
};
// 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: string
title: 'My cool blog post',
content: string
content: '...'
},
streamed: {
comments: any;
}
streamed: {
comments: any
comments: var window: Window & typeof globalThis
window.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
オブジェクトをエクスポートできます。このオブジェクトには、capture
と restore
の 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 Config
Config } from 'some-adapter';
export const const config: Config
config: import Config
Config = {
runtime: string
runtime: '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
}
};
その他多くの機能...
- OPTIONS メソッドが
+server.js
ファイルでサポートされるようになりました - 別のファイルに属するものをエクスポートしたり、+layout.svelte にスロットを配置し忘れた場合に、エラーメッセージが改善されました。
- これで、public 環境変数に app.html でアクセスできるようになりました。
- レスポンスを作成するための新しい テキストヘルパー
- そして多くのバグ修正。完全なリリースノートについては、変更履歴を参照してください。
SvelteKit に貢献し、プロジェクトで使用してくださっている皆様に感謝いたします。以前にも申し上げましたが、Svelte はコミュニティプロジェクトであり、皆様からのフィードバックと貢献がなければ実現できなかったでしょう。