🔬

Async Svelte で Async SSR が使えるようになった

に公開
2026/01/16

まえがき

Asynchronous Svelte、最近ホットですよね。まだ Experimental ですが、 await を含むコンポネントを svelte:boundary なしで SSR できるようになっていたので、紹介したいと思います。

https://github.com/sveltejs/svelte/pull/16748

プロジェクト作成

bunx sv create

忘れずに Experimental の Async Svelte の設定もします。

svelte.config.js
import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://svelte.dev/docs/kit/integrations
  // for more information about preprocessors
  preprocess: vitePreprocess(),

  kit: {
    // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
    // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
    // See https://svelte.dev/docs/kit/adapters for more information about adapters.
    adapter: adapter(),
  },
+  compilerOptions: {
+    experimental: {
+      async: true,
+    },
+  },
};

export default config;

Async SSR なし

Async SSR 以前、Async Svelte において非同期なデータを取得するには、クライアント側で1から実行する必要がありました。

src/routes/+page.svelte
<script lang="ts">
  function sleep(ms: number, context: string) {
    return new Promise((resolve) => {
      console.log(`I'm running at ${context}!`);
      return setTimeout(() => resolve(Math.random()), ms);
    });
  }
</script>

<section>
  {#await sleep(1000, "#await block")}
    Loading #await block...
  {:then data}
    using #await block: {data}
  {/await}
</section>

<section>
  <svelte:boundary>
    {#snippet pending()}
      Loading svelte:boundary...
    {/snippet}
    await inside component: {await sleep(1500, "await inside svelte:boundary")}
  </svelte:boundary>
</section>

これは以下の点で理想的ではないです:

  • クライアントに JavaScript を渡してパースされた JS がまたリクエストを飛ばすのを待たなければいけず、初期描画に時間がかかる
  • クライアントが JavaScript をオフにしていた場合データフェッチができない

そのため、 Async Svelte においてもデータフェッチは +page.server.ts や +page.ts で行うのがよいとされていました。

Async SSR

Async SSR があると、 await を svelte:boundary ツリーの外で使えるようになります。すると、 SSR は、その await を待ち、結果を SSR に含むようになります。

src/routes/async-ssr/+page.svelte
<script lang="ts">
  function sleep(ms: number) {
    return new Promise((resolve) => {
      console.log(`I'm running!`);
      return setTimeout(() => resolve(Math.random()), ms);
    });
  }
</script>

await outside component: {await sleep(2000)}

このページの読み込みに 2 秒かかるのがわかると思います。代わりに、返ってくる HTML にはすでに描画結果が含まれています:

$ curl localhost:5173/async-ssr
# 部分抜粋
  <body data-sveltekit-preload-data="hover">
    <div style="display: contents"><!--[--><!--[--><!----><!----><!---->await outside component: 0.6573334609064831<!----><!----><!----><!--]--> <!--[!--><!--]--><!--]-->

Async SSR 以後は、 svelte:boundary (+{#snippet pending()}) の内側はクライアントで遅延フェッチ、外側は SSR 時にフェッチという使い分けになるでしょう。

実際のユースケース

先程の async-ssr の例は、ブラウザで開くと 2 秒後に乱数が変わる (= sleep がサーバーとクライアントで合わせて2回実行される) のがわかると思います。これでは実際のアプリケーションに使ったときも 2 回データフェッチが起こって効率的ではありませんね。

Async SSR のディスカッション では、 elliott-with-the-longest-name-on-github は以下のように説明しています:

This really isn’t very desirable behavior. During hydration, you typically want to stick with the stuff you rendered on the server unless it actually needs to change! What we need to be able to do is render something asynchronously on the server, then hydrate it synchronously on the client based on what we rendered on the server. This requires serialization at some point. Somehow, we have to get the data you’ve rendered over the wire in such a way that we can retrieve it on the client and hydrate it into reactive JavaScript.

React solves this problem by making the serialization boundary be the client component. Data passed from a server component to a client component has to be serializable so that it can be sent over the wire and used in hydration. The downside of this approach is that React has to live in two worlds, where some of your components are server components (and therefore only run on the server) and some of them are client components (and therefore run on both the server, for initial renders, and the client for subsequent client-side ones). Svelte has always been a very isomorphic framework — we believe the Svelte code you write should be able to run equally well on the server and the client. Keeping to these roots, we think the more logical place to put this serialization boundary is at the resource level — at the actual place you’re fetching the data.

Thankfully, we’ve already taken care of this problem for you with remote functions in SvelteKit. Given a setup like this:

export const getUser = query(v.string(), (id) => getUserFromDatabase(id))
<script>
 import { getUser } from './get-user.remote.js';
 import { page } from '$app/state';
</script>

<h1>Hi, {(await getUser(page.params.userId).name}</h1>

…when server rendering, the result of getUser will be inlined into the resulting HTML, and during hydration, getUser will simply return the data synchronously — no delays or additional requests involved.

“But Elliott, I don’t want to use SvelteKit!” That’s fine! Remote functions have been our exploration into what the best possible version of data-fetching in SvelteKit could be. Now that we’ve had that exploration, we’re working on bringing some of that goodness into Svelte core, so that other framework authors can build similar solutions, or you can make your own.

雑訳:

この挙動は期待している挙動とは違うよね。
SvelteKit の Remote Functions を使うと、その結果が HTML にシリアライズされるので、ハイドレーションの遅延やリクエストの重複がなくなるよ!
SvelteKit 以外でもこれを作りやすくするために、 Svelte のコアにこの "goodness" を持ってこようとしているところ。

らしいので、 Remote Function を使いましょう。

あとがき

不正確な点・わかりにくい点などあればぜひコメントしてください!

更新履歴

2026-1-16: Zenn.dev が Svelte のハイライトに対応した (ありがとうございます!) のに伴い、ハイライトを HTML から Svelte に変更しました

Discussion