JavaScript / TypeScript のデバッグは、ベテランと中堅を最も明確に分ける技能のひとつです。console.log をひたすら書き散らして、画面リロードを 100 回繰り返す――そんな「祈りのデバッグ」を卒業し、Chrome DevTools のブレイクポイント / Source Map / Node Inspector / Performance プロファイラ / メモリスナップショットを駆使して問題を 1 発で特定できるようになると、開発速度は文字通り 5 倍以上跳ね上がります。
本記事は ECMAScript 2025 / Chrome 130+ / Node.js 22 LTS / VSCode 1.94+ / Vite 6 / Webpack 5 / Vitest 2 準拠で、コピペで動く 45 以上のコードサンプルを通して、console API の進化系(table / group / time / trace / count / assert / dir / profile)→ Chrome DevTools 全パネルの実戦活用 → ブレイクポイント 6 種(line / conditional / logpoint / DOM / XHR / Event Listener)→ Source Map の仕組みと Webpack / Vite 設定 → debugger キーワード / step in / over / out → Async Stack Traces → Node.js Inspector(--inspect / --inspect-brk) → VSCode + Chrome 統合デバッグ → React / Redux DevTools → Performance / Memory / Coverage / Lighthouse → Sentry / DataDog 連携まで、ツール・実践デバッグの全領域を完全網羅します。
本記事は「ツール・実践デバッグ観点」に特化しているため、JavaScript エラー処理完全実践ガイド(try/catch・Result 型観点)、Promise 完全実践ガイド(非同期処理観点)、async/await 完全実践ガイド(非同期構文観点)、TypeScript よくあるエラーTOP30と解決法(型エラー観点)と相互補完的に読むと、エラー検知から原因特定までの動線が一気通貫で身につきます。
- 1. デバッグの全体像と「祈りのデバッグ」からの卒業
- 2. console API の進化形 ―― log だけで終わらせない
- 2.1 console.table: 配列・オブジェクトを表で見る
- 2.2 console.group / groupCollapsed: 階層化ログ
- 2.3 console.time / timeEnd / timeLog: 処理時間計測
- 2.4 console.trace: 呼び出し元を辿る
- 2.5 console.count / countReset: 呼び出し回数カウント
- 2.6 console.dir / dirxml: オブジェクトと DOM の構造表示
- 2.7 console.assert: 条件が偽の時だけ出す
- 2.8 console.error / warn / info / debug の使い分け
- 2.9 console.profile / profileEnd / timeStamp
- 2.10 文字列フォーマット指定子
- 2.11 本番ビルドで console を自動削除する(ESLint + Vite/Webpack)
- 3. Chrome DevTools 全パネルの実戦活用
- 4. ブレイクポイントを 6 種類使い分ける
- 5. Source Map ―― 圧縮コードを「元のコード」で読むしくみ
- 6. ステップ実行を完全マスター ―― step in / over / out / Async Stack
- 7. Watch Expression / Scope / Local 変数の確認
- 8. Network パネル徹底活用 ―― API デバッグの主戦場
- 9. Performance プロファイラ ―― 「なんとなく重い」を数字に変える
- 10. Memory パネル ―― メモリリーク調査の定石
- 11. Node.js のデバッグ ―― Inspector で Chrome と接続する
- 12. VSCode + Chrome の統合デバッグ
- 13. React / Redux DevTools の活用
- 14. 本番エラーをデバッグする ―― Sentry / DataDog 連携
- 15. CSS / レイアウトのデバッグ
- 16. Coverage タブで「使われていないコード」を見つける
- 17. 実戦シナリオ別デバッグレシピ
- 18. デバッグスキルを伸ばし続けるためのキャリアパス
- 19. まとめ ―― 「祈り」から「観測」へ
1. デバッグの全体像と「祈りのデバッグ」からの卒業
console.log("ここまで来た") を 100 行に散りばめてリロードする「祈りのデバッグ」は、原因特定に時間がかかるだけでなく、本番にログが残るという二次災害も招きます。本章では、まず「デバッグツールを使い分ける」という発想に切り替えるところから始めます。
1.1 デバッグの 4 階層を意識する
JavaScript のデバッグは大きく 4 つの階層に分かれます。階層を意識せずに同じ手段を使い回すと、解決まで遠回りになります。
// 階層 1: 値の確認 → console.log / console.table / console.dir
// 階層 2: 制御フロー追跡 → ブレイクポイント / step in/over/out / call stack
// 階層 3: 時系列・性能 → Performance プロファイラ / Network / time / profile
// 階層 4: 構造・リーク → Memory snapshot / Coverage / Heap / Retainer tree
1.2 「ログ追加 → リロード」が遅い本当の理由
1 回のリロードに 3 秒、ログを 1 個足すサイクルが 30 回で 90 秒。さらにビルド待ちが加わると 10 分以上吹き飛びます。ブレイクポイントなら「止めて全変数を一度に見る」ため、サイクル数が 1〜2 回で済みます。
// ❌ 祈りのデバッグ(リロード地獄)
function calc(a, b) {
console.log("a", a);
console.log("b", b);
const x = a * 2;
console.log("x", x);
const y = b + x;
console.log("y", y);
return y;
}
// ✅ ブレイクポイントなら 1 行も足さずに全変数が見える
function calc(a, b) {
const x = a * 2;
const y = b + x;
return y; // ← ここに line breakpoint を置けば a, b, x, y を一度に確認できる
}
1.3 「再現条件を最小化する」が最重要スキル
どんなに高度なツールを使っても、再現できないバグは直せません。デバッグの 8 割は「最小再現コードに切り詰める作業」です。MRE(Minimal Reproducible Example)を作る過程で原因が判明することも珍しくありません。
2. console API の進化形 ―― log だけで終わらせない
console オブジェクトには log 以外にも 20 個以上のメソッドが定義されており、用途別に使い分けるだけで可読性とデバッグ効率が劇的に上がります。
2.1 console.table: 配列・オブジェクトを表で見る
// 配列 of オブジェクト → DevTools 上で表として描画される
const users = [
{ id: 1, name: "Alice", age: 28, role: "admin" },
{ id: 2, name: "Bob", age: 34, role: "user" },
{ id: 3, name: "Carol", age: 22, role: "user" },
];
console.table(users);
console.table(users, ["name", "role"]); // 列を絞り込める
2.2 console.group / groupCollapsed: 階層化ログ
// ネストした処理の入れ子をログにそのまま反映できる
console.group("User Fetch");
console.log("step 1: validate input");
console.group("step 2: HTTP request");
console.log("GET /api/users");
console.log("status 200");
console.groupEnd();
console.log("step 3: cache update");
console.groupEnd();
// 初期状態で折りたたんで出したい場合
console.groupCollapsed("Heavy diagnostics");
console.log("...大量の詳細...");
console.groupEnd();
2.3 console.time / timeEnd / timeLog: 処理時間計測
// 同名のラベルでペアリングする
console.time("fetchUsers");
const res = await fetch("/api/users");
console.timeLog("fetchUsers", "after fetch");
const data = await res.json();
console.timeEnd("fetchUsers"); // → fetchUsers: 134.2 ms
// 複数同時計測も可能
console.time("parse");
console.time("render");
// ...
console.timeEnd("parse");
console.timeEnd("render");
2.4 console.trace: 呼び出し元を辿る
// 「この関数、誰から呼ばれてるんだっけ?」を即解決
function inner() {
console.trace("inner called");
}
function middle() { inner(); }
function outer() { middle(); }
outer();
// → inner called
// inner @ app.js:3
// middle @ app.js:5
// outer @ app.js:6
2.5 console.count / countReset: 呼び出し回数カウント
// 「この関数、何回呼ばれてる?」を可視化
function handleClick(id) {
console.count(`click:${id}`);
}
handleClick("save"); // click:save: 1
handleClick("save"); // click:save: 2
handleClick("cancel");// click:cancel: 1
console.countReset("click:save");
handleClick("save"); // click:save: 1
2.6 console.dir / dirxml: オブジェクトと DOM の構造表示
// console.log は DOM 要素を HTML 風に出す
// console.dir は同じ要素を「プロパティツリー」として出す
const el = document.querySelector("h1");
console.log(el); // → ...
風の表示
console.dir(el); // → { id, className, dataset, style, ... } のツリー
// console.dirxml は逆に DOM ノードを XML 風に展開
console.dirxml(document.body);
2.7 console.assert: 条件が偽の時だけ出す
// テスト不要のインラインアサーション
function divide(a, b) {
console.assert(b !== 0, "b must not be zero", { a, b });
return a / b;
}
divide(10, 0); // → Assertion failed: b must not be zero { a:10, b:0 }
divide(10, 2); // 何も出力されない(成功時は無音)
2.8 console.error / warn / info / debug の使い分け
// レベル別に DevTools のフィルタが効く + Sentry 等の取込対象も変わる
console.error("致命的: API key 未設定"); // 🔴 赤
console.warn ("将来非推奨: legacyMode = on"); // 🟡 黄
console.info ("初期化完了: env=production"); // ℹ️ 青
console.debug("詳細トレース: payload=", obj); // verbose レベル(デフォ非表示)
console.log ("通常メッセージ");
2.9 console.profile / profileEnd / timeStamp
// コードからプロファイラを開始 / 終了できる(DevTools の Performance に記録)
console.profile("heavy-render");
heavyRender();
console.profileEnd("heavy-render");
// timeStamp は Performance タイムラインにマーカーを刺す
console.timeStamp("user-clicked-save");
2.10 文字列フォーマット指定子
// printf 風のフォーマット指定子も使える
console.log("user=%s age=%d active=%o", "Alice", 28, { last: "today" });
// CSS スタイルも当てられる(%c)
console.log("%c大事なログ", "color:#fff;background:#c00;padding:2px 6px;border-radius:4px;");
2.11 本番ビルドで console を自動削除する(ESLint + Vite/Webpack)
// .eslintrc.cjs ── 本番だけ no-console を error に
module.exports = {
rules: {
"no-console": process.env.NODE_ENV === "production"
? ["error", { allow: ["warn", "error"] }]
: "off",
},
};
// vite.config.ts ── esbuild の drop で console.* を一括除去
import { defineConfig } from "vite";
export default defineConfig({
esbuild: {
drop: ["console", "debugger"],
},
});
3. Chrome DevTools 全パネルの実戦活用
Chrome DevTools(F12 / Cmd+Opt+I)は単一のツールではなく、20 個以上の専門パネルの集合体です。本章では実務で頻度の高いパネルを 1 個ずつ攻略します。
3.1 Elements パネル: DOM と CSS のライブ編集
HTML / CSS をその場で書き換えて挙動を確認できます。Right-click → Force state で :hover / :focus を固定できるので、ドロップダウンメニューの確認に必須です。
3.2 Console パネル: 単なるログ表示ではない
// Console は REPL でもある ―― 任意の式を評価可能
$0 // 直近に Elements で選択した要素
$_ // 直前の式の評価結果
$$("a") // querySelectorAll の短縮形
copy(JSON.stringify(obj, null, 2)) // 値をクリップボードへ
monitor(fn) // 関数の呼び出しをすべてログに出す
unmonitor(fn) // 解除
monitorEvents(window, "click") // 任意要素の任意イベントを監視
unmonitorEvents(window)
3.3 Sources パネル: コードのナビゲーションと検索
Cmd+P / Ctrl+P でファイル名検索、Cmd+Shift+F / Ctrl+Shift+F で全ファイル横断検索、Cmd+Shift+O / Ctrl+Shift+O で関数名ジャンプ。VSCode と同じ操作感です。
3.4 Network パネル: HTTP 通信の完全な可視化
Throttling(3G / Slow 4G / Offline)、Preserve log(ページ遷移を跨いだログ保持)、Disable cache、HAR エクスポートなど、API デバッグの主戦場です。
3.5 Performance パネル: 実行時間の可視化
レコード開始 → 操作 → 停止 で炎グラフ(Flame Chart)が出力され、CPU 時間がどの関数で使われたかを定量化できます。FPS / Layout Shift / Long Tasks もここで確認します。
3.6 Memory パネル: スナップショットとヒープ解析
Heap snapshot を 2 回取って Comparison モードで差分を見るのがメモリリーク調査の定石です。Detached DOM ノードや増え続けるクロージャを Retainer tree で辿ります。
3.7 Application パネル: ストレージ・Service Worker
LocalStorage / SessionStorage / IndexedDB / Cookies / Cache Storage を覗き、Service Worker の登録解除や Manifest の検証もここで行います。
3.8 Lighthouse / Recorder / Coverage
Lighthouse はパフォーマンス監査、Recorder は操作ログから E2E スクリプト生成、Coverage は「読み込んだのに使われていない JS/CSS」を可視化して Bundle 最適化のヒントを与えてくれます。
4. ブレイクポイントを 6 種類使い分ける
ブレイクポイントは「行で止める」だけではありません。条件付き / ログのみ / DOM 変更 / XHR / Event Listener / 例外時の 6 種類を使い分けることで、「祈りのデバッグ」とは比較にならない速度で原因に辿り着けます。
4.1 Line Breakpoint と debugger キーワード
// Sources の行番号クリックで設置する line breakpoint と同等
function processOrder(order) {
validate(order);
debugger; // ← この行で実行が必ず停止する
charge(order);
notify(order);
}
// debugger キーワードは DevTools が開いている時だけ効く
// → 本番に残しても影響なし(ただし lint で no-debugger を有効に)
4.2 Conditional Breakpoint(条件付きブレイクポイント)
行番号を右クリック → Add conditional breakpoint。指定した式が true の時だけ停止します。ループ内の特定要素だけ追跡したい場合に必須。
// 例: items.length が 100 を超えた時だけ止めたい
// 条件式に items.length > 100 と入力
for (const item of items) {
process(item); // ← items.length > 100 の時だけ停止
}
// 例: 特定 ID のユーザーだけ止めたい
// 条件式に user.id === "U-9912" と入力
users.forEach(user => render(user));
4.3 Logpoint(止めずにログだけ出すブレイクポイント)
行番号を右クリック → Add logpoint。コードに console.log を書かずに、外側からログを差し込めます。リロード不要・本番コードを汚さないため、革命的に便利です。
// ソースを書き換えずに、外部から console.log("count:", count) を流し込む
// → リロードせず、保存もせず、本番コードもクリーンなまま
function tick(count) {
// ← この行に logpoint: `tick called: ${count}` を設定
state.value += 1;
}
4.4 DOM Breakpoint(要素変更時に止める)
Elements パネルで要素を右クリック → Break on →「subtree modifications」「attribute modifications」「node removal」。「この要素、誰が消してる?」を秒で特定できます。
4.5 XHR / Fetch Breakpoint
Sources → 右ペイン → XHR/fetch Breakpoints →「+」で URL の一部を入力。例えば /api/users と書けば、その URL を含む通信が発火する瞬間に止まります。誰が API を叩いているか不明な時に。
4.6 Event Listener Breakpoint
Sources → 右ペイン → Event Listener Breakpoints。click / keydown / load / resize など、特定イベントが発火した時に止められます。「クリックすると変な挙動」系の調査に最強。
4.7 Pause on Exceptions
// Sources パネル右上の ⏸ アイコンで切り替え
// ✅ Pause on uncaught exceptions → catch されない例外で止まる
// ✅ Pause on caught exceptions → try/catch されていても止まる(原因特定向け)
// 「どこで例外が出てるかわからない」時はこれを ON にしてリロード
try {
doSomething();
} catch (e) {
// ← Pause on caught exceptions が ON なら ここの throw 行でも止まる
log(e);
}
5. Source Map ―― 圧縮コードを「元のコード」で読むしくみ
本番にデプロイされる JS は webpack / Vite / esbuild / SWC により圧縮・トランスパイルされ、app.a3f9.min.js のような形になります。これを元の .ts / .tsx ファイル上でデバッグできるのが Source Map です。
5.1 Source Map の正体
// 圧縮ファイル末尾の参照コメント
// → ブラウザはこのコメントを見て .map をリクエストする
//# sourceMappingURL=app.a3f9.min.js.map
// .map ファイルの中身(JSON)
{
"version": 3,
"file": "app.a3f9.min.js",
"sources": ["src/index.ts", "src/utils.ts"],
"sourcesContent": ["...元ソース全文..."],
"mappings": "AAAA;AACA;..." // VLQ エンコードされた位置マッピング
}
5.2 Webpack 5 での Source Map 設定
// webpack.config.js
module.exports = {
mode: "production",
// 開発: eval-cheap-module-source-map(高速ビルド+正確な行)
// 本番: hidden-source-map(.map は出すが参照コメントは付けない)
devtool: process.env.NODE_ENV === "production"
? "hidden-source-map"
: "eval-cheap-module-source-map",
};
5.3 Vite 6 での Source Map 設定
// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
build: {
sourcemap: "hidden", // 本番でも .map 生成、参照コメントは出さない
},
});
// 開発時は自動で inline source map が有効(無設定でブラウザに元 .ts が見える)
5.4 devtool オプションの選び方
// 開発用(ビルド速度優先)
// eval-cheap-module-source-map ← 推奨。HMR と相性が良い
// 本番用(セキュリティ重視)
// hidden-source-map ← ファイルは出すがブラウザは自動読込しない
// → エラー監視 SaaS(Sentry等)にアップロード
// source-map ← フル source map(.map URL がブラウザに見える)
// 絶対に使ってはいけない本番設定
// eval / eval-source-map ← 元ソースが production bundle に埋め込まれる
5.5 Sentry / DataDog への Source Map アップロード
// hidden-source-map + Sentry CLI で運用
// 1. ビルド時に .map を生成
// 2. CI で sentry-cli が .map をアップロード
// 3. 本番サーバから .map を削除(またはアクセス制限)
// 4. Sentry がスタックトレースを元 .ts の行番号に復元
// .github/workflows/deploy.yml
- name: Upload sourcemaps
run: |
npx @sentry/cli sourcemaps inject ./dist
npx @sentry/cli sourcemaps upload --release=$GITHUB_SHA ./dist
- name: Delete sourcemaps from server
run: rm dist/**/*.map
5.6 「source map が効かない」あるあるトラブル
ブラウザの DevTools 設定で Enable JavaScript source maps がオフになっている / CDN が .map をブロックしている / CSP が sourceMappingURL を弾いている、の 3 つが頻出。Network タブで .map ファイルが 200 で返っているかを確認しましょう。
6. ステップ実行を完全マスター ―― step in / over / out / Async Stack
ブレイクポイントで停止した後の操作が、デバッグ速度を決定づけます。「F11 連打」だけでは関数の深淵に飲まれてしまいます。4 つのステップ操作を意識的に使い分けましょう。
6.1 Step over (F10) ―― 関数呼び出しを「中に入らず」越える
// ライブラリ関数を 1 行ずつ追う必要はない時に使う
function main() {
const data = fetchData(); // ← F10: fetchData の中には入らず次の行へ
const sliced = data.slice(0, 5);// ← F10
render(sliced); // ← F10
}
6.2 Step into (F11) ―― 関数の「中に入る」
// 自作関数のロジックを追いたい時
function main() {
const result = myCustomLogic(x); // ← F11: myCustomLogic の中に入る
}
function myCustomLogic(x) {
// ← ここから 1 行ずつ追える
if (x < 0) return 0;
return x * 2;
}
6.3 Step out (Shift+F11) ―― 現在の関数から「抜ける」
うっかり Step into で深い関数に入ってしまった時、Shift+F11 で呼び出し元まで一気に戻れます。これを知らずに F10 を 100 回叩くエンジニアが今でも多い。
6.4 Step (F9) と Continue (F8)
// F8 Continue: 次のブレイクポイントまで一気に進む
// F9 Step: 最も細かいステップ(式単位で進む場合もある)
// F10 Step over
// F11 Step into
// Shift+F11 Step out
// 一時的にすべてのブレイクポイントを無効化したい時は
// Sources パネル右上の「Deactivate breakpoints」アイコン
6.5 Call Stack パネルの読み方
停止中、左ペインの Call Stack には現在地までの呼び出し履歴が積まれます。任意のフレームをクリックすると、そのフレームのスコープ変数を確認できます。
6.6 Async Stack Traces ―― async/await を「同期のように」追う
// Chrome は async/await や setTimeout を越えてスタックを連結してくれる
// 設定: Sources → ⚙ → "Disable async stack traces" のチェックを外す(デフォON)
async function outer() {
await Promise.resolve();
await inner();
}
async function inner() {
throw new Error("boom");
}
outer().catch(e => console.error(e));
// 出力されるスタック(async 境界も連結される)
// inner (file.js:7)
// await ↑
// outer (file.js:3)
6.7 Blackbox / Ignore List ―― ライブラリのコードを飛ばす
// node_modules や polyfill に Step in で入りたくない時
// Settings → Ignore List → 正規表現を追加
// 例: /node_modules/ → これだけで React / Vue 内部に入らなくなる
// package.json 経由でプロジェクト固有設定も可能
{
"name": "my-app",
"ignoreList": ["src/legacy/", "vendor/"]
}
7. Watch Expression / Scope / Local 変数の確認
停止中、右ペインには Scope(現在のフレームのローカル変数)、Watch(任意の式の評価結果)、Closure(クロージャの変数)が並びます。
7.1 Watch に式を追加する
// Watch ペインの「+」で任意の式を追加
items.length // 変化を追いたい配列の長さ
items.filter(i => i.active).length // 動的な集計値
performance.memory.usedJSHeapSize // メモリ使用量
document.querySelectorAll(".error").length // エラー要素数
7.2 Console で停止中の変数を弄る
// 停止中、Console は現在のスコープにアクセスできる
// → ローカル変数 user の状態を直接書き換えてから F8 で再開
user.role = "admin";
items.push({ id: 999, name: "fake" });
// ホットフィックスの動作確認や、エラー条件の再現に便利
8. Network パネル徹底活用 ―― API デバッグの主戦場
8.1 リクエストの再現と編集 ―― Copy as fetch / cURL
// Network パネルで任意リクエストを右クリック → Copy
// - Copy as fetch → fetch() 形式の JS コードがクリップボードへ
// - Copy as cURL → ターミナルで再現できる cURL コマンド
// - Copy as PowerShell
// 例: 認証ヘッダ込みの fetch コードがそのまま貼れる
fetch("https://api.example.com/users", {
"headers": { "authorization": "Bearer ey..." },
"method": "GET",
});
8.2 Network Throttling ―― 遅い回線の再現
Network タブのスロットリング設定で Slow 4G / 3G / Offline を選ぶと、開発機からも実ユーザー環境を再現できます。カスタムプロファイルも作成可。
8.3 HAR エクスポート / インポート
// 全リクエストを 1 ファイル(.har)に書き出して共有
// Network パネル → ⬇ アイコン → Save all as HAR with content
// 受け取った側は同じパネルにドラッグ&ドロップして再現
// → 「自分の環境では再現しないバグ」を相手の通信ログそのままで調査できる
8.4 Override(ローカルでレスポンスを差し替える)
// バックエンドが未完成でも、フロントの動作確認ができる
// Sources → Overrides → ローカルフォルダを選択
// Network で対象レスポンスを右クリック → "Save for overrides"
// → そのファイルを編集すると、リロード後ブラウザは編集後 JSON を読む
// 本番 API のレスポンスを差し替えて再現テストする時に最強
9. Performance プロファイラ ―― 「なんとなく重い」を数字に変える
9.1 プロファイル取得の基本フロー
// 1. Performance パネルを開く
// 2. ⚫ Record をクリック
// 3. 重い操作を実行(例: 大きな表をソート)
// 4. ⏹ Stop で記録停止
// 5. Flame Chart / Bottom-Up / Call Tree を眺める
// コードからも自動的に開始できる(2.9 参照)
performance.mark("sort-start");
sortHeavyTable();
performance.mark("sort-end");
performance.measure("sort", "sort-start", "sort-end");
// → Performance パネルの User Timing トラックに表示される
9.2 Flame Chart の読み方
横軸が時間、縦軸が呼び出し階層。幅の広いブロックが時間を食っている関数です。赤い「Long Task(50ms 超)」マークが付いていたら、最優先で潰すべき重い処理です。
9.3 CPU Throttle で実機を再現
Performance タブ右上の「CPU: No throttling」を 4x slowdown / 6x slowdown に変えると、ローエンド端末での重さを開発機で再現できます。
9.4 User Timing API でカスタム計測
// アプリケーションロジック内に計測点を仕込む
performance.mark("data-fetch:start");
const data = await fetchData();
performance.mark("data-fetch:end");
performance.measure("data-fetch", "data-fetch:start", "data-fetch:end");
// 取得した計測値は performance.getEntriesByType で取り出せる
console.table(performance.getEntriesByType("measure"));
9.5 Web Vitals(LCP / INP / CLS)の取得
// npm i web-vitals
import { onCLS, onLCP, onINP, onFCP, onTTFB } from "web-vitals";
const report = (metric) => {
// 例: 自社ログ基盤に送信
navigator.sendBeacon("/log/vitals", JSON.stringify(metric));
};
onCLS(report);
onLCP(report);
onINP(report); // ← Core Web Vitals 2024 から INP が FID を置換
onFCP(report);
onTTFB(report);
9.6 Lighthouse の活用
// DevTools → Lighthouse タブ → Analyze
// CI でも自動化可能
// npx lighthouse https://example.com --output=json --output-path=./report.json
// CI で performance スコアが 0.9 を下回ったら fail させると効果的
10. Memory パネル ―― メモリリーク調査の定石
10.1 Heap Snapshot 比較の手順
// 1. Memory → Heap snapshot → Take snapshot(① ベースライン)
// 2. 怪しい操作を 10 回繰り返す(例: モーダルを開閉)
// 3. Take snapshot(② リーク後)
// 4. ② を選択し、上部ドロップダウンを "Comparison" に変更
// 5. "Delta" 列で大きく増えているクラスを特定
// 6. 行を展開し Retainer tree でどこから参照されているかを追う
// よくある犯人:
// - removeEventListener し忘れ
// - setInterval clearInterval し忘れ
// - グローバル window.__cache が肥大化
// - Detached DOM(画面から消したのに参照が残った要素)
10.2 Detached DOM Nodes の発見
// Memory パネルの Class filter に "Detached" と入力
// → 「画面から外れたのに JS から参照されたまま」の DOM が一覧で出る
// 典型的な原因コード ─ クロージャに DOM を捕まえさせている
function attach() {
const el = document.getElementById("modal");
document.addEventListener("keydown", (e) => {
// ↑ keydown ハンドラが el を捕まえる
// → モーダルを画面から外しても el は GC されない
if (e.key === "Escape") el.remove();
});
}
// 修正: AbortController で破棄可能にする
function attach() {
const el = document.getElementById("modal");
const ctl = new AbortController();
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
el.remove();
ctl.abort(); // ← リスナを GC 可能にする
}
}, { signal: ctl.signal });
}
10.3 Allocation Sampling で「いつ」確保したかを追う
Memory → Allocation sampling を Record → 操作 → Stop。スタックトレース付きで「どの関数がメモリを確保したか」を時系列で確認できます。
11. Node.js のデバッグ ―― Inspector で Chrome と接続する
11.1 node –inspect / –inspect-brk
# 通常起動(プロセスは即実行、後から接続可能)
node --inspect index.js
# → Debugger listening on ws://127.0.0.1:9229/...
# 最初の行で止めて待つ(起動直後を調査したい時)
node --inspect-brk index.js
# 任意ポートで開く / すべての NIC で listen(リモート接続用、信頼ネットワーク限定)
node --inspect=0.0.0.0:9230 index.js
# TypeScript を直接起動するなら tsx
npx tsx --inspect src/index.ts
11.2 Chrome から Node に接続する
# Chrome のアドレスバーに以下を入力
chrome://inspect
# "Remote Target" に node プロセスが現れる → "inspect" をクリック
# → DevTools が起動し、Sources で TS/JS にブレイクポイントが置ける
# 自動で全 node プロセスを開きたい時
# chrome://inspect → ☑ Discover network targets → Configure...
11.3 nodemon + –inspect で自動再起動デバッグ
// package.json
{
"scripts": {
"dev:debug": "nodemon --inspect --watch src --ext ts,js --exec tsx src/index.ts"
}
}
// → ファイルを保存するたびに自動再起動
// → DevTools の接続も自動で再アタッチされる
11.4 VSCode で Node をワンクリックデバッグ
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Node",
"type": "node",
"request": "launch",
"runtimeExecutable": "node",
"runtimeArgs": ["--import", "tsx"],
"program": "${workspaceFolder}/src/index.ts",
"skipFiles": ["/**", "${workspaceFolder}/node_modules/**"],
"console": "integratedTerminal"
},
{
"name": "Attach to Node",
"type": "node",
"request": "attach",
"port": 9229,
"restart": true
}
]
}
11.5 Vitest のテストをデバッグする
// VSCode launch.json に Vitest 構成を追加
{
"name": "Vitest current file",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "test", "--", "--inspect-brk", "--no-file-parallelism", "${relativeFile}"],
"console": "integratedTerminal"
}
// テストファイル内に debugger; を仕込む or 行ブレイクポイントを置く
// → F5 でテストが起動し、その行で停止する
11.6 Node のメモリリークを掴む heapdump
// Node 22+ では v8 標準でスナップショット取得可能
import v8 from "node:v8";
// 任意のタイミングで .heapsnapshot を書き出す
process.on("SIGUSR2", () => {
const file = `./heap-${Date.now()}.heapsnapshot`;
v8.writeHeapSnapshot(file);
console.log("wrote", file);
});
// 本番で送信: kill -SIGUSR2
// → 取得した .heapsnapshot を Chrome DevTools → Memory → Load profile で開く
12. VSCode + Chrome の統合デバッグ
12.1 ブラウザ側 React/Vite を VSCode で止める
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Chrome against Vite",
"type": "chrome",
"request": "launch",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}",
"sourceMaps": true,
"userDataDir": "${workspaceFolder}/.vscode/chrome-debug-profile"
}
]
}
// 使い方:
// 1. ターミナルで npm run dev
// 2. F5(Launch Chrome against Vite)
// 3. VSCode 上で .tsx に直接ブレイクポイント
// → 画面操作するとそこで停止
12.2 Compound 構成 ―― フロントとバックを同時デバッグ
// .vscode/launch.json
{
"compounds": [
{
"name": "FullStack",
"configurations": ["Launch Node", "Launch Chrome against Vite"]
}
]
}
// F5 で FullStack を選ぶと API サーバとブラウザの両方を一度に起動
// → どちら側で止まっているか問わずブレイクポイントが効く
13. React / Redux DevTools の活用
13.1 React DevTools の Components / Profiler
// 拡張機能をインストール後、DevTools に Components / Profiler タブが増える
// Components: コンポーネントツリーと props/state/hooks を可視化
// Profiler: コミット単位でレンダリング時間を計測 → 不要な再描画の特定
// 開発時のみ表示名を付ける
const MyComponent = (props) => { /* ... */ };
MyComponent.displayName = "MyComponent"; // ← 匿名 forwardRef や HOC で必須
// useEffect / useState の値も Components タブから直接編集できる
// → ステートを書き換えて挙動確認するのに便利
13.2 React Profiler でレンダー回数を可視化
// "Why did this render?" を有効化(Profiler 設定の歯車)
// → コミット時に "props changed", "hook changed" 等の理由が表示される
// Profiler API でコードから取得することも可能
import { Profiler } from "react";
const onRender = (id, phase, actualDuration) => {
if (actualDuration > 16) {
console.warn(`[${id}] ${phase} took ${actualDuration.toFixed(2)}ms`);
}
};
13.3 Redux DevTools ―― アクション履歴とタイムトラベル
// Redux Toolkit ならデフォルトで有効
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== "production",
});
// Zustand から Redux DevTools に乗せる
import { create } from "zustand";
import { devtools } from "zustand/middleware";
const useStore = create(devtools((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 }), false, "inc"),
})));
14. 本番エラーをデバッグする ―― Sentry / DataDog 連携
14.1 Sentry の最小セットアップ(React)
// main.tsx
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
release: import.meta.env.VITE_RELEASE, // hidden source map と一致させる
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({ maskAllText: true }),
],
});
14.2 Error Boundary でクラッシュを Sentry に送る
// React で必須のフォールバック
import * as Sentry from "@sentry/react";
function App() {
return (
エラーが発生しました: {error.message}
}
onError={(error, info) => {
console.error(error, info);
}}
>
);
}
14.3 ユーザー情報・タグを添付してフィルタしやすく
// ログイン時にユーザーをセット
Sentry.setUser({ id: user.id, email: user.email });
// 機能フラグや実験 ID をタグにする
Sentry.setTag("feature.newCheckout", "on");
Sentry.setContext("experiment", { variant: "B", bucket: 17 });
// パンくず(breadcrumb)を手動で残す
Sentry.addBreadcrumb({
category: "ui.click",
message: "save button clicked",
level: "info",
});
15. CSS / レイアウトのデバッグ
15.1 Layout Shift Regions を有効化
DevTools → ⋮ → More tools → Rendering →「Layout Shift Regions」をチェック。CLS(Cumulative Layout Shift)発生箇所が一瞬青く光ります。
15.2 Paint Flashing / FPS Meter
同じ Rendering メニューの「Paint flashing」を ON にすると、再描画された領域が緑にフラッシュします。「動かしていないのに再描画されている」箇所の発見に。
15.3 Flexbox / Grid デバッガ
/* Elements パネルで display: flex/grid の要素に "flex" / "grid" バッジが表示される */
/* バッジクリックで Flex/Grid のガイドオーバーレイ表示 */
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
/* → DevTools のオーバーレイで「3 列が正しく描画されているか」を視覚的に確認 */
16. Coverage タブで「使われていないコード」を見つける
// DevTools → ⋮ → More tools → Coverage
// "Start instrumenting coverage and reload page" をクリック
// → リロード後、JS/CSS の各ファイルで「未使用バイト数 / 全バイト数」が表示される
// 80% 以上未使用なら Code Splitting の余地が大きい
// → import() で動的ロードへ移行する候補
const LazyChart = React.lazy(() => import("./Chart"));
function Dashboard() {
return (
<Suspense fallback={loading...
}>
);
}
17. 実戦シナリオ別デバッグレシピ
17.1 「無限ループで画面が固まる」
// 症状: タブが応答なし。Performance タブも開けない。
// 手順:
// 1. DevTools の Sources で "Pause on exceptions" を ON
// 2. Esc で Console を開いた状態でリロード
// 3. ⏸ Pause script execution を連打(F8)
// → 止まった場所が無限ループの内側
// 4. Call Stack を遡って原因の useEffect / while を特定
// よくある犯人(React)
useEffect(() => {
setCount(count + 1); // 依存配列 [count] で無限ループ
}, [count]);
// 修正: 関数型 update + 依存配列削除
useEffect(() => { setCount(c => c + 1); }, []);
17.2 「Cannot read property ‘x’ of undefined」
// 1. Pause on caught exceptions を ON にしてリロード
// 2. 例外発生時に停止 → Call Stack / Scope で undefined だった変数を確認
// 3. ?. と ?? で防御するか、上流で必ず初期化するか判断
// 修正前
const name = user.profile.name;
// 修正後
const name = user?.profile?.name ?? "(未設定)";
17.3 「fetch が 401 を返す」
// 1. Network タブで該当リクエストを選択
// 2. Headers で Authorization が付いているか確認
// 付いていない場合: トークン取得処理の問題
// 付いている場合: トークンの中身を jwt.io でデコードして期限を確認
// 3. Copy as fetch でブラウザに戻す → トークンだけ最新版に差し替えて再実行
// → 401 が消えればトークン期限切れ確定 → リフレッシュ処理を直す
17.4 「画面が真っ白(Blank Screen)」
// 1. Console に赤いエラーが出ているか確認(出ていれば真因)
// 2. React なら Error Boundary が握り潰している可能性 → fallback を console.error 付きに
// 3. main.tsx / index.tsx の createRoot 周りで例外が出ていないか
// 4. のソース URL が 200 で返っているか Network で確認
// 5. や 自体が空 → CSP / JS 無効化を疑う
17.5 「Production だけバグる」
// チェックリスト(出やすい順)
// 1. NODE_ENV / import.meta.env の差分
// 2. minify による予約語衝突や Tree Shaking のしくじり
// 3. source map がアップロード/参照されていない → スタックが読めない
// 4. CDN キャッシュ汚染(古い chunk が残っている)
// 5. CSP / CORS / Referrer Policy が本番だけ厳しい
// 6. 開発時の MSW モックが本番で外れて API レスポンス形式が違う
// 一時的に開発ビルドを本番デプロイして切り分けるテクも有効
// vite build --mode development
18. デバッグスキルを伸ばし続けるためのキャリアパス
ここまで紹介した技術は、独学でも書籍やドキュメントを真剣に読み込めば習得可能です。しかし「実戦のバグ現場で使いこなす」感覚は、レビューを受けながら数百件単位でこなしてようやく身体化されます。とりわけ Source Map 設定、メモリリーク調査、本番エラーの追跡はチーム開発の現場でなければ経験を積みにくい領域です。
体系的に学びたい・実務で武器にしたいなら、現役エンジニアによる伴走型レッスンが圧倒的に近道です。テックアカデミーはフロントエンドコース・React コース・Node.js コースで現役エンジニアのマンツーマンレビューが受けられ、DevTools / Source Map / Performance チューニングまでカリキュラムに組み込まれています。「ブレイクポイントの種類は知っているのに、いざ本番バグに対面すると手が動かない」レベルから「最初の 30 分で原因を特定できる」レベルへ短期間で引き上げてくれます。
転職を視野に入れているなら 侍エンジニア の現役エンジニア×専属マンツーマン体制がおすすめ。デバッグ能力は 面接でコードレビューを受けた時に最も差が出る スキルで、ライブコーディング課題で「console.log の山」を書いてしまう候補者は即落ちる傾向があります。コードレビュー込みのレッスンで「停止して読む」習慣が身につけば、転職市場での評価が一段階上がります。
未経験から正社員エンジニアを目指す方は DMM WEBCAMP の転職コースが王道。20 代なら転職保証付きで、6 か月でモダンフロントの実務スキルとデバッグ手法を網羅できます。経済産業省認定の Reskilling 補助金で実質負担を大幅に抑えられる点も大きい。
すでに実務経験があり、より高単価・モダンな案件に身を置きたいフリーランス志望なら レバテックフリーランス が業界最大手の安心感。React / TypeScript / Node.js の高単価案件が日々更新され、案件によっては 「Sentry / DataDog でのエラー解析担当」「Performance 改善 PJ」といったデバッグスキルが直接武器になるポジションも多数。担当エージェントが希望年収・稼働日数に沿って案件をマッチングしてくれます。
19. まとめ ―― 「祈り」から「観測」へ
本記事では、console API の使い分けから始まり、Chrome DevTools の主要パネル、6 種類のブレイクポイント、Source Map の仕組みと Webpack / Vite 設定、ステップ実行と Async Stack Traces、Watch / Scope、Performance / Memory / Coverage、Node.js Inspector(--inspect / --inspect-brk)、VSCode + Chrome 統合デバッグ、React / Redux DevTools、Sentry / DataDog 連携まで、JavaScript / TypeScript デバッグのツールと実戦を完全網羅しました。
覚えておきたいエッセンスは次の 7 つです。
- 「祈りのデバッグ」を捨てる ―― リロード地獄からブレイクポイントへ
- console は
logだけじゃない ――table/group/time/trace/count/assertを使い分ける - ブレイクポイントは 6 種類 ―― 特に
logpointと Conditional は革命的 - Source Map は「hidden-source-map + Sentry へアップロード」が本番の正解
- Async Stack Traces を有効化して await を越えてスタックを追う
- Heap Snapshot の Comparison でメモリリークの差分を見る
- node –inspect-brk + Chrome / VSCode で Node も同じ操作感でデバッグ
デバッグ力は、エンジニアの市場価値を最も「静かに」しかし「決定的に」押し上げるスキルです。今日からひとつ、ブレイクポイントを置いてみるところから始めてみてください。
合わせて読みたい関連記事:
- JavaScript/TypeScriptエラー処理完全実践ガイド ―― try/catch・Result 型・カスタム Error 観点
- Promise完全実践ガイド ―― 非同期エラーの伝播と AbortController
- async/await完全実践ガイド ―― 並列・直列・キャンセル戦略
- TypeScriptよくあるエラーTOP30と解決法 ―― 型エラー解消の体系的アプローチ
- モダンビルドツール&Webパフォーマンス最適化完全ガイド ―― Vite/Webpack の devtool 設定深堀り

コメント