Svelte 5の非同期処理改善:SvelteKitの既存方式との比較ドキュメントです。
Svelteがついにエンタープライズ級に成長するのでしょうか?
Svelte 5は、Svelte Summit 2024でRich Harrisが発表した**「What Svelte Promises」**セッションを通じて、非同期処理メカニズムの大幅な改善を予告しました。既存のSvelteKit開発者が経験していた非同期データローディングの不便さ(例:必須のload関数使用、ルーター依存的なデータフェッチング、複雑な型定義、遅い順次処理、複数のローディング画面など)を解決し、コンポーネントレベルで簡潔で直感的な非同期コードを書けるようになったのです。このセクションでは、SvelteKit(Svelte 3/4)での既存方式とSvelte 5で新しく導入される方式を実際の開発シナリオに例えて比較し、それぞれが開発体験をどのように向上させるかを見ていきます。
SvelteKitの伝統的なデータローディングは、ページファイルのload関数またはコンポーネントのonMount/{#await}ブロックを活用する方式でした。例えば、ページコンポーネントでデータを取得するために+page.tsにexport async function load()を実装してfetchを実行した後、ページSvelteコンポーネントでは渡されたpropsを使用しました。このアプローチはSSR(サーバーサイドレンダリング)を通じて初期データを事前にレンダリングする利点がありましたが、開発者の立場からはいくつかの不便さがありました:
複数箇所に分離されたロジック:ページコンポーネントのUIとデータロジックが分離されていて、**一つのファイルですべての流れを把握するのが困難でした。**簡単なデータリクエストでも、load関数ファイルとSvelteコンポーネントファイルの2箇所を行き来する必要がありました。
ルーター依存性:load関数はSvelteKitルーティングに縛られていてページ単位でのみ動作するため、一般的なコンポーネントでは使用できず、ページ以外のコンポーネントはonMountで別途データを取得する必要がありました。これはルーターと無関係なコンポーネントの再利用性を低下させました。
複雑な型定義:TypeScript環境でload関数の戻り値は、SvelteKitが経路ベースで生成するPageData型で定義されますが、このために別途のジェネリック型宣言や.d.ts設定が必要でした。このように型生成が面倒で、自動生成された型を開発者が完全に理解するのが難しく、**「型の小細工(type shenanigans)」**という指摘もありました。
順次処理による遅延:load関数やonMount内部で連続した非同期作業がある場合、基本的に順次実行されました。開発者が明示的にPromise.allなどを使用しなければ、複数のfetchが直列に発生してローディングが遅くなる問題がありました。
複数のローディング状態:複数のコンポーネントがそれぞれ非同期処理をすると、それぞれ個別にローディングスピナーを管理する必要がありました。例えば、ページの親コンポーネントと子コンポーネントがそれぞれデータを取得する場合、画面に2つ以上のローディングUI(「スピナー」)が表示されたり、データ到着時点が異なってUIが順次ガタガタする現象が発生しました。これを解消するには、データを一箇所ですべて読み込んで下位にpropsとして渡すprop-drillingが必要でしたが、このパターンはコード構造を硬直させました。
上記の問題を実際の事例で考えてみると、ブログ投稿ページを実装する時を思い出すことができます。SvelteKitでは投稿内容とコメントリストを読み込むために以下のように書く必要がありました:
// +page.ts - 既存のSvelteKit方式
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch, params }) => {
const postRes = await fetch(`/api/posts/${params.id}`);
const commentsRes = await fetch(`/api/posts/${params.id}/comments`);
return {
post: await postRes.json(),
comments: await commentsRes.json()
};
};
<!-- +page.svelte -->
<script>
export let data; // load関数から渡されたデータ
const { post, comments } = data;
</script>
<h1>{post.title}</h1>
<p>{post.content}</p>
<CommentsList {comments} />
この方式はSSRで初期にpostとcommentsをすべて受け取って最初の画面にレンダリングする利点がありますが、開発者はすべての関連データを一箇所で事前にロードする必要があります。もしCommentsListコンポーネント内部で自分でデータを取得するようにすると、SSRの利点が消えてローディングUIを両方管理する必要がありました。
既存方式の不便さ:上記のコードで見られるように、load関数に複数のfetchを並べると基本的に順次実行されるため、最初のfetchが終わってから2番目のfetchが行われます。開発者が意識的にPromise.allを適用しない限り、並列でデータを読み込めずに遅延が発生します。また、すべてのデータをloadで処理していると、使用しないデータまで不必要にロードしたり(propsとして受け取っても実際には使われない場合)、後でコード整理時にどのデータが使われているか一目で把握しにくく、**「不要なロジックが残っている」**問題が生じることもあります。
上記のような理由で、SvelteKitの既存の非同期戦略は**開発の利便性の面で限界を示してきました。**今度は、Svelte 5でこれらの問題をどのように解決するか、改善事項ごとに見ていきましょう。
**Svelte 5では、コンポーネントの<script>
領域で直接awaitを使用できるようになりました。**つまり、コンポーネント自体を非同期関数のように扱って非同期データを直接取得できます。これは「最小限のセレモニー(minimal ceremony)」原則に合致し、フレームワーク専用APIなしで普通のJavaScriptコードを書くように使用できる機能です。開発者はもはやページごとのload関数やonMountに依存せず、必要な場所で直接データをfetchできるようになりました。
例えば、先ほど言及したブログ投稿ページをSvelte 5方式に変えてみると次のようになります:
<!-- Svelte 5: ページコンポーネント内で直接await -->
<script>
import CommentsList from './CommentsList.svelte';
export let id; // ページルーターから渡された投稿ID
const postRes = fetch(`/api/posts/${id}`);
const commentsRes = fetch(`/api/posts/${id}/comments`);
// 並列でfetchリクエストを開始した後、各結果を待つ
const post = await postRes.then(r => r.json());
const comments = await commentsRes.then(r => r.json());
</script>
<h1>{post.title}</h1>
<p>{post.content}</p>
<CommentsList {comments} />
上記のコードで見るように、コンポーネントスクリプトの最上位でawaitを使用してデータを取得しました。Svelte 5コンパイラはこのようなコードを処理するためにコンポーネントを非同期でレンダリングするメカニズムを提供し、開発者はまるで同期コードを書くように作成すればよいです。過去の方式と比較した改善点は次のとおりです:
単一ファイルにロジック集中:load関数ファイルがまったく必要なくなるか最小化されます。UI構成とデータリクエストロジックが一箇所にあるので、コードの可読性が高まり管理が楽になります。
ルーター依存性からの脱却:コンポーネント内部で自由にデータを読み込めるので、必ずしもページ単位でなくてもどのコンポーネントでも自分で非同期データを処理できます。例えば、<CommentsList>
コンポーネントも必要であれば自分でawait fetch(...)を使用してデータを取得できます(既存ではページloadで受け取ってpropsとして渡すか、内部onMountで処理する必要がありました)。
型処理の単純化:const post = await fetch().then(r=>r.json())
のように使用すると、postの型は一般的なTS推論によって決定されます。別途のPageDataインターフェースを気にする必要がなく、fetchレスポンスにジェネリック型を指定するなど慣れ親しんだTypeScript方式で型を扱えます。結果的に開発者がSvelteKit専用の型生成ルールを学習したり、複雑なジェネリック宣言を悩む時間を減らしてくれます。
コード量の削減:不要なボイラープレートがなくなります。load関数のcontextオブジェクトからfetchやparamsを抽出し、いちいち返すオブジェクトを構成するコードの代わりに、必要なデータを直接変数に割り当てて使用できます。Rich HarrisもSvelte 5について「皆さんはより少ないコードを書くことになるでしょう」と言及しましたが、このような簡素化されたデータローディング方式がその一例です。
要約すると、コンポーネント内部awaitサポートで開発者は望むデータを望む時点で取得するロジックを直感的に作成でき、SvelteKitのルーティングやルールに縛られなくなります。これはすぐにメンテナンスの利便性と開発生産性の向上につながります。
**参考:**現在Svelte 5では、コンポーネント最上位await機能が実験的に導入されており、
<svelte:boundary>
という特別なコンポーネント(https://svelte.dev/docs#svelte_boundary)で非同期領域を囲んで**ローディング中に表示するUI(pending snippet)**を指定できます。例えば、上記のコンポーネントを<svelte:boundary pending={<p>ローディング中...</p>}> ... </svelte:boundary>
で囲むと、データがすべて準備されるまでは「ローディング中...」文句だけ表示し、準備完了時に内部をまとめてレンダリングできます。このようにSvelte 5は既存の{#await ... then ...}
ブロックなしでも簡単にローディング状態を表示できる方法を提供します。
Svelte 5の非同期処理哲学の核心は**「自動調整(automatic coordination)」です。これは非同期作業と状態変化をフレームワークが自動で同期して処理**するという意味で、開発者が手動でUI状態を管理するために複雑なコードを書く必要が大幅に減ります。具体的に、ある非同期作業が完了するまで関連するUI更新を保留することで、ユーザーに一貫した画面遷移を提供できるようになります。
実戦シナリオとして、投稿詳細ページで投稿内容を表示しながらユーザーコメントをローディングする状況を再び考えてみましょう。既存のSvelteKit方式なら親コンポーネントが投稿を表示した後、子<CommentsList>
コンポーネントがonMountでコメントをfetchしてローディングが終わったらリストをレンダリングしました。この場合、ユーザーは初期に投稿は見えるがコメント位置にはローディングスピナーが回っている画面を見ることになります。コメントデータ到着後にリストが表示されますね。親と子がそれぞれローディング状態を管理するために生じる一般的なUXパターンです。または先ほどのようにすべてのデータを一度に取得する場合は、2つのデータのうち遅い方のために全体が遅延したり、逆にSSRで一度に描かれたなら開発の利便性が犠牲になりました。
Svelte 5の自動同期レンダリングを活用すると、親と子コンポーネントの非同期作業をフレームワークが自動で並列処理し、結果をまとめて一度に画面に反映します。例えば、Svelte 5では次のような流れが可能です:
親コンポーネントがpostデータをawaitで取得し、同時に子に渡すcommentsPromiseも生成(fetch呼び出し)します。この時2つのfetchは並列で進行され、親はpostが準備されるまで、子はcommentsが準備されるまでそれぞれ待機します。
コンポーネントが<svelte:boundary>
で囲まれていれば、すべての非同期作業が完了するまで既存の画面を維持するか共通ローディングUIだけ表示します。非同期処理完了時点で親-子コンポーネントすべて同時に最新データでレンダリングされるので、中間に中途半端な空の画面や複数のスピナーが表示されません。
もし非同期待機中に他の状態が変更されたら(例:ユーザーが画面上の他のインタラクションをした)、その部分は即座に更新されますが、非同期作業結果と衝突しないよう処理されます。つまり、待つべきものは待ちながらも、待つ必要のない変化はリアルタイムで反映する賢い動作をします。
結果的に、Svelte 5では開発者が別途気にしなくても非同期データローディングによるUIの不安定さが大幅に減ります。ユーザーの立場では、すべてのデータが準備されて一貫した完成形の画面を見るか、少なくとも一度に遷移する画面を見ることになって体験が向上します。開発者の立場では、過去に**「どの時点でどのローディング表示をオンオフするか」、「親-子間のデータローディング順序をどう合わせるか」などの悩みをいちいちコードで解決していた負担がなくなります。Svelteコンパイラとランタイムがこのような調整を引き受けてくれるので、私たちは核心ロジックに集中**できます。
例えば、Svelte 5の自動同期レンダリングのおかげで以下のような比較が可能です:
シナリオ | 既存のSvelteKit(Svelte 4) | Svelte 5方式 |
---|---|---|
投稿+コメント同時ロード | - SSR load 関数で2つのデータをすべてfetch- 親が先にレンダー後、子領域にローディングスピナー表示 - 子データ到着時に交換 |
- 親でpost await、子でcomments await(並列処理)- <svelte:boundary> で共通ローディングUI表示- 完了時に同時にレンダリング |
中間状態の露出 | - 投稿だけ先に、コメントは空の画面など中間状態露出 | - 完全な画面だけ露出またはローディングUIだけ表示 |
ローディングUIの数 | - 親/子それぞれスピナー管理必要 | - 一つの<svelte:boundary> で統合可能 |
上記の比較のように、Svelte 5の自動同期レンダリングは開発者にはシンプルなコードで優れたUXを実装させてくれ、ユーザーはよりスムーズな画面遷移を体験するようにしてくれます。Rich Harrisはこのようなアプローチについて、ReactやVueなどが導入したSuspenseと類似の概念を言及しながらも、Svelteはコンパイラ主導設計を通じてより良いergonomicsと少ない欠点でコンポーネントレベルの非同期を提供できると自信を持ちました。
Svelte 5では非同期処理と反応性の面でパフォーマンス最適化が自動的に内蔵されています。2つのキーワードは**「並列by default」と「fine-grained reactivity(細分化された反応性)」**です。
Svelte 5以前は、開発者が意識的に非同期呼び出しを並列で実行するようコーディングしなければ、JavaScriptのawaitは順次実行を引き起こしました。例えば、onMount内でawait fetchA(); await fetchB();
をするとBはAが終わった後でなければ実行されませんでした。**Svelte 5はこのような部分までもコンパイル段階で最適化して、基本的に並列で実行されるように変えました。**例えば、Svelte 5コンパイラは上記のコードを内部的に次のように変換できます:
// Svelte 5コンパイラが内部的に処理する方式(例)
const _p1 = fetchA();
const _p2 = fetchB();
const [resultA, resultB] = await Promise.all([_p1, _p2]);
つまり、開発者が順次的にawaitを書いても実際には2つのfetchが同時に進行されるようにするのです。この**「自動並列化」はSvelte 5の非同期処理設計要件の一つとして明示されています(「template内の式は純粋(pure)だと仮定できるので、順次的なawaitが必ず順次的な作業を意味しないよう最適化できる」)。同様に、複数の兄弟コンポーネントの非同期作業も自動的に並列実行されます。これは開発者が並列処理のために別途コードを書いたりリファクタリングしなくても最適なローディング速度を得られるという意味です。結果的に全体的なアプリの反応速度向上**とともに、遅いネットワーク環境でも不要な遅延を最小化してくれます。
Svelteは元々も変化が発生した部分だけDOMを更新する効率的な反応性で有名です。Svelte 5ではさらに一歩進んで反応性の追跡単位をより細かくしました。新しいルーン(rune)ベースの状態管理($state、$derivedなど)導入で、オブジェクトのプロパティ単位まで追跡して変更された部分だけ再評価および再レンダーできるようになりました。例えば、Svelte 4まではオブジェクトobj全体を交換するかストアAPIを使用しなければ内部プロパティ変更を反応的に処理できましたが、Svelte 5ではobj.someProp = newValue
だけで該当プロパティを使用するUIだけ更新する形です。Rich Harrisはこれについて、Svelte 5のコンパイラが**「言語レベルの細かい制御と効率的な状態更新」**を可能にしたと説明します。
このfine-grained reactivityは先ほど言及された**「coarse-grained invalidation(広範囲な無効化)」問題を解決します。既存のSvelteKit load方式では、ページレベルでデータをすべてまとめていたため、小さな変更でも全体のload結果が無効化されて再計算される事態が起きました。しかし、Svelte 5では各awaitや各反応性状態が独立的に追跡されて必要な時だけ更新されます。例えば、ユーザーが現在見ている投稿リストで一つの項目の「いいね」数だけ増加しても、過去には全体リストデータを新しくしたり開発者が手動最適化する必要がありましたが、今は該当項目のいいね数字部分だけ更新される形です。これはDOM更新および演算量を大幅に減らしてパフォーマンスを向上**させ、開発者が不要な最適化コードを書かなくても効率的な動作を得られるようにします。
また、Svelte 5の反応性改善は開発者体験も高めてくれます。例えば、$stateで宣言された状態はコンポーネントのローカル状態とSvelteストアの概念を統合したものと見ることができますが、おかげで既存のSvelteKitでグローバル状態管理のためにストアを乱用したり、またはローカル状態を維持しようと複雑にコーディングしていた部分がシンプルになることが期待されます。まとめると、Svelte 5の細分化された反応性はパフォーマンスとDX(開発者体験)二兎を得る変化です。
最後に、先ほど見た改善事項を中心に既存のSvelteKit方式とSvelte 5方式を比較して要約します:
改善要素 | 既存のSvelteKit(Svelte 3/4) | Svelte 5 |
---|---|---|
データローディング方式 | ページ専用load関数でデータfetch → コンポーネントにpropsとして注入。またはコンポーネントonMount/{#await}ブロック内部で処理(SSR未対応)。 | コンポーネント<script> 上端で直接await使用可能。フレームワークAPIなしで一般変数割り当てのようにデータロード。 |
ルーティング依存性 | データロジックがルーターに縛られていてコンポーネント再利用困難。(ページでないコンポーネントは自体ローディング時SSR特典放棄必要) | ルーター依存性減少 – どのコンポーネントでも必要ならawait fetch使用。ページ/レイアウト区分なく同じパターン活用。 |
型とボイラープレート | SvelteKitが生成するPageData型に従って別途定義必要。load結果を構造分解してpropsとして受け取るなど反復コード発生。 | 一般的なTS型推論使用。コード一箇所で処理するのでボイラープレート減少。型安全性も自然に確保。 |
非同期実行方式 | 基本的にawait呼び出しは順次処理される。(並列処理には開発者の明示的考慮必要) | 基本並列処理 – 順次awaitも内部的に同時実行最適化。複数コンポーネントのfetchも並列進行(ネットワーク遅延短縮)。 |
ローディング中UI | 複数のspinners/プレースホルダーがそれぞれ表示される可能性。(開発者が手動で組み合わせなければ一貫したローディング画面可能) | <svelte:boundary> でローディング画面一元化可能。自動同期レンダリングで中間不完全状態をユーザーに見せない。 |
反応性/更新範囲 | load無効化時全体ページデータ更新(coarse-grained)。コンポーネントローカル状態もオブジェクトの場合全体交換必要。 | fine-grained reactivityで変更された部分だけ更新。不要な再演算減少、より良いパフォーマンスとスムーズなUX。 |
上記の表で見られるように、**Svelte 5は既存の欠点を綿密に補完して開発者にはシンプルで楽しいコーディング体験を、ユーザーにはより良いパフォーマンスとUXを提供する方向に進んでいます。**実際にRich HarrisもSvelte 5について「すべての面でより良くなった(just better in every way)」フレームワークだと強調しましたが、これは単純な宣伝文句ではなく、先ほど見たこのような実質的な改善が裏付ける自信でしょう。
最後に、このような変化はSvelteKitにも大きな影響を与えます。Svelteチームは既存のload関数などのprimitivesから徐々に脱却する計画を示し、Svelte 5とともにより直感的なデータフェッチングパターンが登場すると見られます。要約すると、**Svelte 5の非同期処理改善は過去のSvelteKit開発が抱えていた不便さを解消することで、開発者は少ないコードでより多くのことができ、ユーザー体験も向上するwin-winを達成しました。**今後、Svelte 5を活用した開発では、複雑だった非同期ロジックに割く時間が減り、アプリケーション本来のロジックと機能実装により集中できるでしょう。
**参考文献:**Rich Harris、「What Svelte Promises」、Svelte Summit 2024;Svelte公式ブログおよびRFCドキュメント(Asynchronous Svelte議論)など。