「App Routerに変えたのに遅い」問題
Next.js App Routerに移行したら、自動的にサイトが速くなると思っていませんか。
残念ながら、そうはなりません。App Router自体は強力なアーキテクチャですが、正しく使わないとPages Routerより遅くなるケースすらあります。
私たちTufe Companyが手がけたプロジェクトでも、App Router移行直後にLighthouseスコアが30点下がったケースがありました。原因を調査して最適化した結果、最終的には移行前より20点以上改善。Performanceスコア94、LCPは1.2秒まで持っていけました。
この記事では、その過程で得た知見を全部共有します。
Server Componentsの力を正しく引き出す
App Routerの最大の武器はServer Componentsです。でも、この使い分けを間違えている人が本当に多い。
Server Componentsがデフォルトという意味
App Routerでは、すべてのコンポーネントがデフォルトでServer Componentになります。ブラウザではなくサーバー側でレンダリングされ、HTMLだけがクライアントに送られる。
つまり、JavaScriptバンドルにコンポーネントのコードが含まれない。これがパフォーマンス改善の核心です。
"use client" を安易に書かない
ここが最大の落とし穴。useStateやuseEffectを使いたいからと、コンポーネントの先頭に"use client"を書く。すると、そのコンポーネントとその子コンポーネントすべてがClient Componentになる。
// ❌ こうしがち — ページ全体がClient Componentに
"use client"
export default function ProductPage() {
const [count, setCount] = useState(0)
return (
<div>
<ProductDetails /> {/* これもClient Componentになる */}
<Reviews /> {/* これもClient Componentになる */}
<AddToCartButton count={count} setCount={setCount} />
</div>
)
}
// ✅ こうする — インタラクティブな部分だけClient Component
export default function ProductPage() {
return (
<div>
<ProductDetails /> {/* Server Component のまま */}
<Reviews /> {/* Server Component のまま */}
<AddToCartButton /> {/* これだけ "use client" */}
</div>
)
}
私たちの経験則では、Client Componentはページ全体の20%以下に抑えるのが理想。ボタンのクリック、フォームの入力、アニメーションなど、本当にブラウザ側の処理が必要な部分だけに限定します。
Server Components(デフォルト)
サーバーでレンダリング、JSゼロ
Streaming SSRで体感速度を劇的に改善する
Server Componentsと並んで強力なのがStreaming SSRです。
従来のSSRとの違い
従来のSSR(Pages Router)では、ページのすべてのデータが揃ってからHTMLをまとめて送信していました。APIの応答が遅いコンポーネントが1つあると、ページ全体の表示が遅れる。
Streaming SSRでは、準備ができた部分から順次HTMLを送信します。ヘッダーとナビゲーションは即座に表示されて、データの取得が必要な部分はloading.tsxのフォールバックUIを表示しながら、裏でデータを取得して完了次第差し替える。
Suspenseを戦略的に配置する
loading.tsxはルートセグメント単位のフォールバックですが、より細かい制御には<Suspense>を直接使います。
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
<h1>ダッシュボード</h1>
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel /> {/* DBクエリに2秒かかる */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* 外部API呼び出しに3秒かかる */}
</Suspense>
<RecentActivity /> {/* キャッシュ済みで即座に表示 */}
</div>
)
}
ポイントは、遅いコンポーネントを個別にSuspenseで囲むこと。そうすることで、速いコンポーネントの表示が遅いコンポーネントに引きずられなくなります。
画像最適化 — next/imageを正しく使う
Webパフォーマンスの問題の約60%は画像に起因します。next/imageコンポーネントは強力ですが、設定を間違えると効果が半減します。
sizes属性を必ず指定する
sizesを省略すると、Next.jsはビューポート幅100%を前提にした画像を生成します。サイドバーの中にある300px幅の画像に、1920px幅の画像が読み込まれる。無駄です。
// ❌ sizesを省略
<Image src="/hero.jpg" width={800} height={600} alt="hero" />
// ✅ sizesを正しく指定
<Image
src="/hero.jpg"
width={800}
height={600}
alt="hero"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
/>
priority属性はファーストビューの画像だけ
priorityを指定すると画像がプリロードされますが、ファーストビュー(スクロールせずに見える範囲)の画像だけに使ってください。それ以外の画像に付けると、逆にLCPが悪化します。
外部画像のドメイン設定
外部のCDNやCMSから画像を読み込む場合、next.config.jsのimages.remotePatternsに許可するドメインを設定してください。設定しないとnext/imageの最適化が効きません。
フォント最適化 — next/fontの活用
Webフォントの読み込みは、CLSとLCPの両方に影響します。next/fontを使えば、これを自動的に最適化できます。
import { Noto_Sans_JP } from 'next/font/google'
const notoSansJP = Noto_Sans_JP({
subsets: ['latin'],
weight: ['400', '700'],
display: 'swap',
preload: true,
})
export default function RootLayout({ children }) {
return (
<html lang="ja" className={notoSansJP.className}>
<body>{children}</body>
</html>
)
}
next/fontはビルド時にフォントファイルをダウンロードしてセルフホスティングします。Google Fontsへの外部リクエストが不要になるので、接続のオーバーヘッドがゼロに。
日本語フォントはファイルサイズが大きいので、weightを必要な太さだけに限定するのが大事です。全ウェイト(100〜900)を読み込むのは絶対に避けてください。
画像最適化
AVIF/WebP自動変換、レスポンシブサイズ、遅延読み込み
フォント最適化
next/fontでゼロレイアウトシフト。必要なウェイトのみ読み込み
Streaming SSR
Suspenseで部分的にレンダリング。ユーザーは待たない
Core Web Vitals
LCP, FID, CLSを継続的にモニタリングし改善
Core Web Vitalsの改善チェックリスト
最後に、私たちが実際のプロジェクトで使っているチェックリストを共有します。
LCP(Largest Contentful Paint)を2.5秒以内にする
- ファーストビューの画像に
priorityを設定しているか - Server Componentsで不要なJSバンドルを削減しているか
- 外部フォントをnext/fontでセルフホスティングしているか
sizes属性で適切なレスポンシブ画像を配信しているか
INP(Interaction to Next Paint)を200ms以内にする
- 重いClient Componentを動的インポート(
next/dynamic)で遅延読み込みしているか - イベントハンドラ内で重い処理をメインスレッドで実行していないか
useTransitionを使って非緊急な状態更新を後回しにしているか
CLS(Cumulative Layout Shift)を0.1以内にする
- 画像に
widthとheightを明示しているか - Webフォントの読み込みで
display: 'swap'を使っているか - 動的に挿入されるコンテンツ(広告、バナー等)にスペースを確保しているか
パフォーマンスは「後から直す」では手遅れになる
Webサイトのパフォーマンスは、設計段階で決まります。完成してから「遅いから直して」では、アーキテクチャレベルの変更が必要になることが多く、コストが膨大になる。
App Routerは正しく使えば圧倒的に速いサイトを作れます。逆に、なんとなく使うと従来より遅くなることもある。
大事なのは、パフォーマンスバジェットを最初に決めて、開発中に継続的に計測すること。LCP 2.5秒以内、INP 200ms以内、CLS 0.1以内。この数字をチーム全員が意識しながら開発する。
Webサイトのパフォーマンス改善にお困りの方へ。 Tufe Companyでは、Next.js / App Routerを活用したパフォーマンス最適化の支援を行っています。Lighthouseスコアの改善からCore Web Vitalsの継続的なモニタリングまで、お気軽にご相談ください。