## ボトルネックを特定するための詳細な手順
ボトルネックを特定するには、「どこで」「どれくらい」時間がかかっているのかを定量的に計測し、切り分けていく作業が不可欠です。以下の3つのステップで進めていきましょう。
### ステップ1:サーバーサイド(Remix Loader)の処理時間を計測する 🎯
まず、ユーザーが最も影響を受ける「ページの表示が始まるまでの時間(TTFB: Time To First Byte)」を遅くしている原因が、サーバー側のどこにあるのかを突き止めます。Remixのloader
関数内で、処理を細かく区切って時間を計測するのが最も効果的です。
console.time()
と console.timeEnd()
を使うと、コードの特定区間の実行時間をミリ秒単位で簡単に計測できます。
app/routes/your-route.tsx
の loader
関数の例:
import { json } from "@remix-run/node";
import { dbA } from "~/dbA.server"; // Prisma Client for DB A
import { dbB } from "~/dbB.server"; // Prisma Client for DB B
export const loader = async () => {
// 1. loader全体の処理時間を計測開始
console.time("loader: total execution");
// 2. DB-Aからのデータ取得時間を計測
console.time("loader: fetch from DB-A");
const dataFromA = await dbA.yourModel.findMany({
// ...クエリ条件
});
console.timeEnd("loader: fetch from DB-A");
// 3. DB-Bからのデータ取得時間を計測
console.time("loader: fetch from DB-B");
const dataFromB = await dbB.otherModel.findMany({
// ...クエリ条件
});
console.timeEnd("loader: fetch from DB-B");
// 4. 取得したデータの整形・紐付け処理の時間を計測
console.time("loader: data shaping/merging");
const combinedData = dataFromA.map(itemA => {
const correspondingItemB = dataFromB.find(itemB => itemB.id === itemA.bId);
return { ...itemA, ...correspondingItemB };
});
console.timeEnd("loader: data shaping/merging");
// loader全体の処理時間を計測終了
console.timeEnd("loader: total execution");
return json({ data: combinedData });
};
【計測方法】
- 上記のように
loader
関数内にconsole.time()
を仕込みます。 -
remix dev
で開発サーバーを起動します。 - ブラウザで対象ページにアクセスします。
- 開発サーバーを起動しているターミナルのログを確認します。
【ログの確認と判断】
ターミナルに以下のようなログが出力されます。
loader: fetch from DB-A: 850.123ms
loader: fetch from DB-B: 50.456ms
loader: data shaping/merging: 5.789ms
loader: total execution: 908.234ms
-
fetch from DB-A
やfetch from DB-B
の時間が大半を占める場合: ボトルネックはデータベースのクエリ実行そのものです。次のステップ2に進みます。 -
data shaping/merging
の時間が長い場合: ボトルネックはサーバーサイドでのデータ整形ロジックです。特に、ループのネストが深い、複雑な計算をしている場合に発生します。アルゴリズムの見直しが必要です。 -
全体的に時間がかかっているが、個々の処理はそれほどでもない場合: 複数の処理を直列で実行している(
await
を連続で呼んでいる)ことが原因かもしれません。Promise.all
を使って並列化することを検討します。
// 並列化の例
console.time("loader: fetch all dbs");
const [dataFromA, dataFromB] = await Promise.all([
dbA.yourModel.findMany({ ... }),
dbB.otherModel.findMany({ ... }),
]);
console.timeEnd("loader: fetch all dbs");
### ステップ2:データベース(Prisma)のクエリを分析する 🔍
ステップ1でDBクエリが遅いと判明した場合、次はその原因を深掘りします。
1. Prismaのログ機能を有効化する
Prisma Clientの初期化時にログオプションを追加すると、実行されたSQLクエリや所要時間を確認できます。
app/db.server.ts
の例:
import { PrismaClient } from "@prisma/client";
let db: PrismaClient;
declare global {
var __db: PrismaClient | undefined;
}
if (process.env.NODE_ENV === "production") {
db = new PrismaClient();
} else {
if (!global.__db) {
global.__db = new PrismaClient({
// このログ設定を追加!
log: [
{ emit: 'stdout', level: 'query' },
{ emit: 'stdout', level: 'info' },
{ emit: 'stdout', level: 'warn' },
{ emit: 'stdout', level: 'error' },
],
});
}
db = global.__db;
}
export { db };
これを設定して再度ページにアクセスすると、ターミナルに以下のようなログが出力されます。
prisma:query SELECT ... FROM "YourModel" WHERE ...
// クエリ実行にかかった時間も表示される
2. インデックスの確認
取得に時間がかかっているクエリが特定できたら、そのSQLのWHERE
句やJOIN
句で使われているカラムにデータベースのインデックスが適切に設定されているかを確認してください。これはパフォーマンス問題の最も一般的な原因の一つです。特に大量のレコードから特定の条件で絞り込む際に、インデックスがないとフルテーブルスキャンが発生し、劇的に遅くなります。
### ステップ3:フロントエンド(Chrome開発者ツール)で通信状況を可視化する 📊
サーバーサイドの処理が速くても、データの転送自体に時間がかかっている可能性もあります。これはChrome開発者ツールの「Network」タブで確認します。
- 開発者ツールを開き、「Network」タブを選択します。
- スロットリング設定(例: "Slow 3G")を有効にします。
- ページをリロードします。
- 一覧から一番上にあるドキュメントリクエスト(URLが
/your-route
のようなもの)をクリックします。 - 「Timing」タブを開きます。
ここで注目すべきは「Waiting (TTFB)」です。
-
Waiting (TTFB) の時間が長い: サーバーがレスポンスを生成するのに時間がかかっている証拠です。これは、Remixの
loader
関数全体の処理が遅いことを意味します。ステップ1と2に戻ってサーバーサイドの処理を改善する必要があります。 -
Content Download の時間が長い: サーバーからのレスポンスは速いが、レスポンス(HTMLやJSONデータ)のサイズが大きく、ダウンロードに時間がかかっている状態です。Prismaの
select
句を使って、フロントエンドで本当に必要なデータだけを返すようにし、データ転送量を削減できないか検討してください。
## 他に考えられるボトルネックの要因
上記の基本的な調査に加えて、以下のような点もボトルネックになり得ます。
-
N+1問題: データを整形するループの中で、さらにDBへの問い合わせ(
await
)が発生していませんか?これはループの回数だけクエリが発行され、パフォーマンスを著しく低下させる典型的なアンチパターンです。 - サーバーリソースの不足: コンテナ化された環境で、RemixサーバーやDBコンテナに割り当てられたCPUやメモリが不足している場合、処理が遅くなることがあります。特に大量のデータをメモリ上で結合する際には、メモリ使用量が増大します。
-
Prisma Clientのインスタンス化:
loader
が呼ばれるたびにnew PrismaClient()
を実行していませんか?Prisma Clientはインスタンス化のコストが高いため、シングルトン(アプリケーション全体で一つのインスタンスを使い回す)として利用するのがベストプラクティスです。 - フロントエンドのレンダリング: データ取得は速くても、取得した37件のデータをReactがコンポーネントとして描画する処理自体が重い可能性もゼロではありません。これは開発者ツールの「Performance」タブでプロファイリングすることで確認できます。
## まとめ:ボトルネック調査の進め方
- まず、
console.time()
をloader
に仕込んで、サーバーサイドのどこが遅いのか(DBクエリか、データ整形か)を特定しましょう。これが最も簡単で効果的な第一歩です。 - DBクエリが遅いとわかったら、PrismaのログでSQLを確認し、インデックスの有無をチェックします。
- サーバーサイドの処理時間に問題がないように見えたら、Chrome開発者ツールのTimingタブでTTFBとContent Downloadの時間を確認し、問題がサーバー側かネットワーク転送にあるのかを切り分けます。
ブラウザ上での検証
## 準備 🔬:計測前のセットアップ
まず、ボトルネックを正確に計測するための準備をします。
- シークレットモードで開く: ブラウザの拡張機能などが計測結果に影響を与えないように、シークレットウィンドウで対象のページを開きます。
-
開発者ツールを開く:
- Windows:
F12
またはCtrl+Shift+I
- Mac:
command+option+I
- Windows:
- パフォーマンスタブを選択: 開発者ツールの上部にあるタブから「パフォーマンス」を選びます。
-
CPUとネットワークのスロットリングを設定(推奨):
- CPU: ⚙️歯車マークをクリックし、「CPU」のドロップダウンから「4倍速低下」などを選びます。これにより、性能の低いスマートフォンでの処理をシミュレートでき、問題がより明確になります。
- ネットワーク: 「ネットワーク」のチェックボックスをオンにし、「低速3G」などを選びます。
## ステップ1:計測の開始と停止 🔴
準備ができたら、いよいよページのパフォーマンスを記録します。ページの読み込み時のパフォーマンスを計測するのが一般的です。
- タブの左上にある、丸い矢印のついた記録ボタン(①)(
ページを再読み込みしてプロファイリングを開始します
)をクリックします。これが一番簡単で確実です。 - ページが自動的にリロードされ、パフォーマンスの記録が開始されます。
- ページが完全に表示されたら、手動で「停止」ボタン(②)をクリックします。
## ステップ2:全体像の確認 - フレイムチャートを読む 🔥
停止すると、画面にグラフが表示されます。これが「フレイムチャート」です。最初は情報量に圧倒されるかもしれませんが、見るべきポイントはシンプルです。
- 横軸: 時間の経過です。右に行くほど時間が進みます。
- 縦軸: 呼び出しスタックです。上にある処理が、すぐ下にある処理を呼び出したことを意味します。
- グラフの幅: 幅が広いほど、その処理に時間がかかったことを示します。ボトルネックは、幅の広いブロックです!
主に「Main」と書かれたセクションに注目してください。 ここがブラウザがJavaScriptの実行やページのレイアウト計算など、ほとんどの作業を行う場所です。
色の意味をざっくり理解する
- 🟡 黄色 (スクリプト実行): JavaScriptの処理です。
loader
から受け取ったデータを加工したり、Reactがコンポーネントを描画したりする処理はここに含まれます。ここがボトルネックになっていることが多いです。 - 🟣 紫色 (レンダリング): スタイル計算やレイアウトの計算です。複雑なCSSや多数のDOM要素があると長くなります。
- 🟢 緑色 (ペイント): 実際にピクセルを画面に描画する処理です。
まず探すべきサイン
フレイムチャートの中で、右上に赤い三角形 🚩 が付いているタスクを探してください。これは「長いタスク(Long Task)」と呼ばれるもので、ブラウザが「この処理は長すぎて、ユーザー操作を妨げた可能性があるよ!」と教えてくれているサインです。まずはこの赤い三角形の付いた、幅の広い黄色いブロックを探しましょう。
## ステップ3:ボトルネックの特定 - 原因の関数を見つける 🎯
幅の広い「長いタスク」を見つけたら、その正体を突き止めます。
-
ボトルネックと思われる、幅の広い黄色いブロックをクリックします。
-
すると、開発者ツール下部のペインに、そのタスクの詳細が表示されます。「サマリー」タブが選択されているはずです。
-
次に、下部ペインのタブを「ボトムアップ」に切り替えます。
-
「合計時間」の列をクリックして、時間が長い順に並べ替えます。
一番上に表示された関数が、あなたのアプリケーションのパフォーマンスを最も低下させている原因(犯人)です!
「ボトムアップ」タブは、「どの関数が、合計で一番長い時間CPUを占有したか」を一覧表示してくれる非常に強力な機能です。
- 一覧に表示された関数名の右側にある
app.js:123
のようなリンクをクリックすると、ソースコードの該当箇所にジャンプでき、どの処理に時間がかかっているのかを直接確認できます。
## まとめ:ボトルネック発見までの最短ルート
- 準備: シークレットモードで開き、CPUとネットワークのスロットリングを設定する。
- 計測: 「パフォーマンス」タブの再読み込みボタンで計測を開始し、表示後に「停止」する。
- 発見: 「Main」セクションで、赤い三角形が付いた、幅の広い黄色いブロックを見つける。
- 特定: そのブロックをクリックし、下部ペインを「ボトムアップ」タブに切り替える。「合計時間」でソートし、一番上の関数がボトルネックの原因。
この手順で、アプリケーションのどこに時間がかかっているのかを具体的に特定できるはずです。ぜひ試してみてください。