2026年10月25日、AI coding agentにaron.twをNext.jsからSvelteKitへ移してもらい、公開後にさらにAstroへ移行した。読者から見たサイトはほぼ同じで、検索、カテゴリ、言語切り替えも残っている。一方、検索ページの外部JavaScriptは155.7 KBから15.7 KBになった。

残したい機能を削らず、ブラウザがダウンロードして実行するコードを大きく減らせた。これが今回いちばん分かりやすい成果だった。

記事を読むことが中心のサイトで、実行モデルを変えると何が省け、何が残るのかを見てみたかった。当時の規模は5言語、記事の言語版35本、約60の静的ページ。Cloudflare Pagesに配置し、Pagefind検索、カテゴリ絞り込み、コピーボタン、テーマと言語のメニュー、ホームのポートレートスライダーを備えていた。

これらは個別に扱える操作だった。検索条件を変えても記事ページのメニューを更新する必要はなく、次の記事へ移るときに編集中のフォームを引き継ぐこともない。Astroを試せる条件がそろっていた。

既存の動作検証も引き継いだ。AstroとSvelteKitの静的・操作中画面210組を比較し、ピクセル差はゼロ。約223件のE2E受け入れ仕様は維持し、フレームワークの内部実装に依存する一部だけを調整した。機能が残ったかどうかを、性能の数値とは別に確かめられた。

HTMLが届いた後も、ブラウザは何をしているか

三つとも静的配置でHTMLを返す。違うのは、その後の処理だ。

Next.jsはビルド時にServer Componentsを実行し、HTMLとクライアント遷移用のpayloadを用意する。Client Componentsにはhydrationが必要になる。SvelteKitもページをhydrateし、サイト内の遷移をclient routerが引き継ぐ。こうした仕組みは、ブラウザ側で状態や操作を継続するためにある。Next.jsの静的出力、SvelteKitのページ設定

このサイトのAstro版は記事をビルド時に処理し、メニューや絞り込みを通常のTypeScriptに任せた。ブラウザでReactやSvelteがページを引き継ぐことはなく、次のHTML文書を読み込んで移動する。

検索ページは三版とも同じPagefindを含むため、機能を削って転送量を減らした比較にはならない。

検索ページの外部 JS 転送量

aron.tw · 2026-10-25

同じ検索機能でも Astro の外部 JS は少なく、HTML 内のスクリプトは別に数えます。

KB(brotli)
  • Next.js155.7
  • SvelteKit60.6
  • Astro15.7
データ表を開く
検索ページの外部 JS 転送量 · KB(brotli)
フレームワークKB(brotli)
Next.js155.7
SvelteKit60.6
Astro15.7

ローカルの Apple M3 Max、幅 390 px、CPU 4 倍スローダウン。Pagefind を含む最初のサンプルの外部 JS response body サイズで、時間の中央値ではありません。HTML 内のスクリプトは除外。KB = 1,024 bytes。

計測データの抜粋と方法

一般の閲覧ページでは、Astroの外部JSは今回およそ0.9–3.2 KBだった。検索は別に数えている。また、小さなスクリプトの一部はHTMLに埋め込まれる。外部ファイルが小さいからといって、JavaScriptがないわけではない。

実行する仕事も減った。幅390 px、CPUを4倍に減速したChromiumで、各ページ5回の中央値を取ると次のようになった。

各ページの中央値の範囲Next.jsSvelteKitAstro
Script実行時間47–88 ms14–35 ms4–14 ms
JS heap3.1–4.2 MB1.6–2.6 MB0.8–0.9 MB

範囲はページ間の差で、一つのページの誤差区間ではない。heapはJavaScriptのメモリだけを表す。このブログでは、転送、実行、保持するメモリを減らせたことに意味がある。

ただしLighthouseのスコアはほとんど変わらなかった。別途行ったLighthouse 13.5.0のモバイルシミュレーションでは、各ページ3回の中央値でFCPが約3–12秒。三版に共通する11個の同期フォントstylesheetが、最初の描画を妨げていた。

次に文字を早く表示するなら、フォントに手を入れる。フレームワークを変えて省けた仕事と、残ったボトルネックが見えた。同じ問題に答えるために、もう一度移行する必要はない。

操作部分は、別の書き方で残る

SvelteKitへの移行では、二つの具体的な作業があった。

記事がHTMLにあるのにhydration payloadにも重複していたため、transport hookと独自の出力処理を組み合わせて、初回の重複を除いた。クライアント遷移用のデータは残している。既存の設定一つで済んだ処理ではない。

また、head内の約18個の高優先度modulepreloadがフォントと帯域を取り合った。当時の低速4GシミュレーションではFCPが0.6–0.9秒遅くなり、fetchpriority="low"で元の水準に戻った。bundleの大きさだけでなく、読み込む順番も効く。

Astroではそのhydration状態を再構成しないため、記事payloadの重複処理は不要になった。それでも検索やカテゴリ絞り込みは実装する必要がある。

記事カードを<template>に描画しておき、条件が変わるとスクリプトで該当カードを複製し、一覧、ページ番号、URLを更新する。ダイアログ、メニュー、ポートレートスライダーもそれぞれスクリプトを持つ。移行完了時点では8個のTypeScriptモジュール、860行だった。

移行記録には、コンポーネントのスクリプトが<template>内へ出力され、自動で動かなかった問題も残っている。当時はメニューのスクリプトを外側で読み込むようにしてイベントを接続した。hydrationがなくても、どこでスクリプトを実行するかは考える必要がある。

現在は各操作を独立して理解できるので、このコードを持つことを受け入れている。検索に編集機能や共有状態が増え、複数の領域が連動し始めたら、純粋なTypeScriptにこだわってDOMを手作業で同期し続けるつもりはない。必要な場所にはSvelteやReactのislandを置ける。

cold buildは短縮、warm buildでは差が縮む

ブラウザ以外では、cold buildが5.81秒から2.55秒に短くなった。

ローカルの cold build 時間

aron.tw · 2026-10-25

ローカルで交互に計測した結果、Astro の cold build は元の Next.js 実装より約 56% 短くなりました。

秒
  • Next.js5.81
  • SvelteKit3.88
  • Astro2.55
データ表を開く
ローカルの cold build 時間 · 秒
フレームワーク秒
Next.js5.81
SvelteKit3.88
Astro2.55

Apple M3 Max、交互に実行した各 3 回の中央値。フレームワークと出力キャッシュを削除し、共通 OG 画像キャッシュは保持。コンテンツ処理とPagefindを含みます。warm buildは続く本文で比較します。

計測データの抜粋と方法

計測にはコンテンツ処理とPagefindも含む。フレームワークのコンパイルだけの時間ではない。続けてビルドするとNext.jsの永続キャッシュが効き、warm buildはNext.js 2.98秒、SvelteKit 4.07秒、Astro 2.63秒だった。coldでの改善を、あらゆる条件で二倍速いという話には広げられない。

コードやパッケージの数でも順位をつけたくない。自分で持つ処理とフレームワークに任せる処理が異なり、行数から工数は出せない。860行を維持する価値があるかは、今後の変更を通して分かってくる。

ページ遷移の数字は、解釈を訂正する

ローカルで記事一覧から本文へ移った時間は、次の通りだった。

ローカル:ブログ一覧 → 記事

aron.tw · 2026-10-25

ローカルの遷移時間は近く、Astro は文書全体を読み込みます。

ms
  • Next.js249
  • SvelteKit244
  • Astro290
データ表を開く
ローカル:ブログ一覧 → 記事 · ms
フレームワークms
Next.js249
SvelteKit244
Astro290

Apple M3 Max、1280 × 900 のデスクトップ幅、CPU 4 倍スローダウン、5 回の中央値、追加のネットワーク制限なし。自動クリックと待機の時間で、INP や厳密なクリックから描画までの時間ではありません。

計測データの抜粋と方法

別の本番記録にはSvelteKit 373 ms、Astro 768 msという各3回の中央値がある。ただし取得したデプロイ時期が違い、CDNやネットワークの状態も、待機したライフサイクルも異なる。Playwrightのlocator.click()は起動した遷移を待つ。文書全体の読み込みとclient routerによる更新を、同じ終点として比較することはできない。

元の記録では約50件のリソースイベントを、大量の304検証と解釈した。この点は訂正が必要だ。requestfinishedにはbrowser cacheから取得したリソースも含まれる。各応答とキャッシュの取得元がなければ、イベント数からネットワーク往復の回数は分からない。

2026年10月25日に保存した本番の確認では、HTMLはmax-age=0, must-revalidate、フォントと抽出した画像は4時間のfreshness、hash付き/_astro/のCSS・JSは1年間のimmutableだった。4時間以内でまだfreshなフォントは、must-revalidateがあるという理由だけで毎回ネットワーク検証されない。この指示が制約するのは期限切れ後の再利用だ。HTTPキャッシュ仕様

文書全体を移動すればHTMLの解析とDOMの構築は再び行う。それでもCSS、フォント、画像はブラウザのキャッシュを使える。CDNのhitは別で、応答が利用者に近くなる一方、ブラウザからの要求自体は発生する。この区別なしに遅れの原因は決められない。

このサイトではhover prefetchも有効にしている。Astro 7.4.2は対応環境で<link rel="prefetch">により次のHTMLを先に取るが、次ページの全リソースや操作が準備済みになるわけではない。当時の300 ms hoverテストで処理時間が改善しなかったことは、その試行の結果であり、prefetch全般が無意味という結論ではない。

次に比較するなら、冷えた/温まったbrowser cacheを固定し、先読み完了を確認し、同じ可視コンテンツの表示時点を測る。JSとビルドの改善は既存データで分かる。遷移は別途、条件をそろえて確かめたい。

このブログでは、Astroを続ける

読むことが中心で、操作を局所的に分けられる現在のサイトには、ビルド時にページを完成させ、必要なコントロールだけブラウザに任せる構成が合っている。

ページ間で多くの操作状態を保ったり、複数領域を一緒に更新したりするなら、Svelteのリアクティブなコンポーネントやクライアント遷移には具体的な役割がある。SvelteKitもcsr=falseでhydrationとrouterを止められるが、Svelteに依存していた操作は別の形で用意する必要がある。

既存のNext.jsなら、client boundaryやルートごとの分割を先に確認する選択もある。今回はServer Actionsやstreamingを測っておらず、移行が元の実装の改善より割安だとも証明していない。AIと既存テストが今回の実験を可能にしたが、すべてのチームに同じ投資を勧めるものではない。

aron.twでは、Astroを残し、小さな操作部分を維持し、文字の表示を遅らせているフォントを直す。フレームワークの選択はいったんここまでにして、サイトの次の仕事へ進みたい。