React Server ComponentsでNext.jsのバンドルサイズを削減する

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

サーバーファースト・アーキテクチャへの移行

10年以上にわたり、Reactはほぼ完全にブラウザ上で動作してきました。私たちは、巨大なJavaScriptバンドルを配信し、ユーザーেরデバイスにレンダリング前のコードの解析、コンパイル、実行を強いることに慣れてしまっていました。過去6ヶ月間にNext.jsのApp Routerで3つの本番プロジェクトをリリースした結果、React Server Components(RSC)のおかげで、Lighthouseのパフォーマンススコアが60台半ばから安定して98以上に跳ね上がるのを目の当たりにしました。

従来のSPAが限界に達する理由

標準的なシングルページアプリケーション(SPA)では、機能を追加するたびに重さが増していきます。たとえ一つの小さなコンポーネントのために date-fns のような重いユーティリティをインポートしたとしても、ブラウザはそのライブラリ全体をダウンロードしなければなりません。この肥大化は「Time to Interactive(TTI)」の低下を招き、SEOにも悪影響を及ぼします。私の経験では、一般的なダッシュボードはビジネスロジックを追加する前でさえ、簡単に500KBのJavaScriptまで膨れ上がってしまいます。

React Server Componentsはこの状況を一変させます。RSCは厳密にサーバー上で実行され、生のコードの代わりに、UIの軽量なJSON風の記述をクライアントに送信します。その結果、ユーザーのデバイス上でのそのコンポーネントのJavaScriptフットプリントは、正確に0バイトになります。

これは単なる新機能ではありません。 格安のAndroid端末や不安定な4G回線でも一瞬で表示されるWebアプリを構築するために必要な、マインドセットの転換なのです。

モダンなNext.jsスタックのセットアップ

現在、RSCを活用するための標準的な方法はApp Routerです。バージョン13.4から安定版となり、サーバーを第一級市民として扱います。この環境では、app/ ディレクトリ内に作成するすべてのファイルが、デフォルトでサーバーコンポーネントになります。

クイックスタート

新しいプロジェクトの立ち上げは1分もかかりません。以下のコマンドを実行して、最初から適切なアーキテクチャを構築しましょう。

npx create-next-app@latest my-rsc-app --typescript --tailwind --eslint

プロンプトが表示されたら、App Routerに対して「Yes」を選択してください。これにより、サーバーとクライアントのロジックを分離するという面倒な作業を自動的に処理してくれるディレクトリ構造がセットアップされます。

0KBバンドルのための設計

成功の鍵は、UIをインタラクティブな部分と静的な部分にどのように分割するかにあります。最近の本番アプリでは、コンポーネントロジックの75%をサーバーに移行することに成功し、ユーザー入力を処理する「末端(leaf)」コンポーネントだけをクライアントに残しました。

サーバーデフォルトの活用

app/inventory/[id]/page.tsx のようなファイルはデフォルトでサーバーコンポーネントです。async/await を使用してデータを直接取得できるため、初期ロードのために useEffect複雑な状態管理を使用する必要が完全になくなります。

// app/products/page.tsx
import { db } from '@/lib/db';

export default async function ProductsPage() {
  // このデータベース呼び出しはサーバー上に留まります
  const products = await db.product.findMany(); 

  return (
    <div>
      <h1>商品カタログ</h1>
      <ul>
        {products.map((p) => (
          <li key={p.id}>{p.name} - ${p.price}</li>
        ))}
      </ul>
    </div>
  );
}

機密性の高いデータベースクライアントがブラウザに触れることはありません。ユーザーはクリーンなHTMLとわずかなメタデータのみを受け取るため、シークレット情報は安全に保たれ、バンドルはスリムなまま維持されます。

インタラクションの処理

サーバーコンポーネントはクリックイベントを監視したり、ローカルの状態を管理したりすることはできません。トグルやフォームが必要な場合は、ファイルの先頭に "use client" ディレクティブを配置してクライアントコンポーネントを作成します。

"use client";

import { useState } from 'react';

export default function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count} 個のアイテムを選択中
    </button>
  );
}

これらのクライアントコンポーネントは可能な限り小さく保ちましょう。データの大部分は親のサーバーコンポーネントで取得し、これらのインタラクティブな「末端」には単純なPropsのみを渡すようにします。

スマートなデータ取得

以前は、ネストされたコンポーネントが連続して遅いAPIコールをトリガーする「ウォーターフォール」問題に苦労していました. RSCでは、データが必要な場所で正確に取得できます。Next.jsはこれらのリクエストを自動的に重複排除するため、1つのページで同じユーザーデータに対してAPIを2回叩く心配をする必要はありません。

このアプローチは「バケツリレー(prop drilling)」を解消します。フッターにプロフィール画像が必要だからといって、5つの階層にわたってユーザーオブジェクトを渡す必要はもうありません。

影響の測定

測定できないものは管理できません。アーキテクチャが実際にバイト数を節約しており、誤ってサーバー側のコードがクライアントに漏れていないかを確認する必要があります。

Bundle Analyzerの使用

私は、出力を視覚化するために @next/bundle-analyzer を活用しています。どのライブラリが予算を消費しているかを正確に浮き彫りにしてくれます。以下のコマンドでインストールします。

npm install @next/bundle-analyzer

next.config.js を更新して有効にします。

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({})

ANALYZE=true npm run build を実行して結果を確認しましょう。あるケースでは、複雑なデータテーブルをサーバーコンポーネントに移行しただけで、メインバンドルから52KBのgzip圧縮されたJavaScript(主に重いフォーマットツール)を削ぎ落すことができました。

RSCペイロードの確認

ナビゲーション中にブラウザの開発者ツールの「Network」タブを確認してください。application/octet-stream タイプの要求が表示されます。これがRSCペイロードです。これは通常、従来のクライアントサイドの手法で同じコンテンツをレンダリングするのに必要なJavaScriptよりも80〜90%小さくなります。

セーフティネットの追加

誤ってデータベースのシークレット情報をクライアントコンポーネントにインポートしてしまうのはよくあるリスクです。開発中にこれらのミスをキャッチするために、server-only パッケージを使用しましょう。

npm install server-only

APIやDBユーティリティファイルに import 'server-only'; を追加します。もしクライアントコンポーネントがこれらをインポートしようとすると、ビルドが即座に失敗し、潜在的なセキュリティ漏洩を防ぐことができます。

結論

React Server Componentsは、ブラウザとの関係性を変えます。私たちはもはやクライアント内で動作する重いアプリを作っているのではなく、サーバーによって支えられた軽量なインターフェースを構築しているのです。パフォーマンスの向上は本物であり、最初の学習曲線を乗り越えれば開発者体験も非常にクリーンになります。まずはブログや商品一覧のような、最も静的なページから移行を開始し、バンドルサイズが消えていくのを実感してください。

Share: