JavaScriptパフォーマンス計測完全実践ガイド〜performance API・Web Vitals・Benchmark.js・Profiler・RUM【2026年版】〜

「ページが遅い気がするけど、どこから手を付ければ?」「console.timeでザックリ測ってるけど、本当に正確?」「Web Vitalsを計測してSentryやGA4に飛ばす方法が分からない」——JavaScriptのパフォーマンス計測は、ツールが多すぎて初心者がつまずきやすい領域です。

本記事では、ブラウザのPerformance APIWeb VitalsBenchmark.js / tinybench / mitataChrome DevTools ProfilerLighthouse CINode.js perf_hooks / clinic.js、さらにSentry / DataDog / GA4へのRUM送信まで、現場で実際に使える計測手法を40本以上のコピペ可能なJS/TSコードで解説します。「測れない最適化は祈り」をやめて、数値で語れるエンジニアになりましょう。

  1. 1. JavaScriptパフォーマンス計測の全体像
    1. 1-1. 「計測しない最適化」は害悪である
    2. 1-2. 計測の3レイヤー(マイクロ・ページ・フィールド)
    3. 1-3. console.timeから卒業しよう
  2. 2. Performance API — 高精度タイマーとUser Timing
    1. 2-1. performance.now() の基本
    2. 2-2. performance.mark() と performance.measure()
    3. 2-3. measureUserAgentSpecificMemory(将来用)
    4. 2-4. PerformanceObserverで非同期に拾う
    5. 2-5. 関数デコレーター化して計測を自動化
    6. 2-6. clearMarks / clearMeasuresで肥大化を防ぐ
  3. 3. Web Vitals — ユーザー体感の3指標を測る
    1. 3-1. Core Web Vitalsの再整理(LCP / INP / CLS)
    2. 3-2. web-vitalsライブラリの導入
    3. 3-3. attribution版で「どこが原因か」まで取る
    4. 3-4. INPの内訳を分解する
    5. 3-5. CLSの原因要素を特定
  4. 4. PerformanceObserver — ブラウザ内部イベントを購読する
    1. 4-1. Navigation Timing(ページ全体の遷移指標)
    2. 4-2. Resource Timing(個別リソースの内訳)
    3. 4-3. Long Tasks API(50ms超のメインスレッド占有)
    4. 4-4. Element Timing(任意要素の描画時刻)
    5. 4-5. Event Timing(個別イベントの遅延)
    6. 4-6. Largest Contentful Paint 単体取得
    7. 4-7. paint タイプ(FP / FCP)
  5. 5. Memory計測 — リーク検出とヒープ監視
    1. 5-1. performance.memory(Chrome系のみ)
    2. 5-2. メモリ増加を時系列ログにする
    3. 5-3. リークパターンを再現するスニペット
    4. 5-4. WeakRef / FinalizationRegistryでGCを観察
  6. 6. マイクロベンチマーク — 関数単位の速度比較
    1. 6-1. Benchmark.jsの基本
    2. 6-2. tinybench(モダンな後継)
    3. 6-3. mitata(超軽量・高精度)
    4. 6-4. ベンチマークの「嘘」を避けるコツ
  7. 7. Chrome DevTools Performance Profiler
    1. 7-1. 録画の基本フロー
    2. 7-2. console.profile() でコードから録画開始
    3. 7-3. CPU Profile JSON の出力と解析
    4. 7-4. Performance.profileを動的に取る(–inspect)
    5. 7-5. Memory Snapshot(ヒープスナップショット)
    6. 7-6. Allocation Profile(タイムライン上の確保)
    7. 7-7. Coverage タブで未使用コードを発見
  8. 8. Lighthouse / Lighthouse CI
    1. 8-1. Lighthouse CLIインストールと実行
    2. 8-2. JSONで構造化結果を保存
    3. 8-3. プログラムから呼ぶ
    4. 8-4. Lighthouse CIで予算管理(lighthouserc.js)
    5. 8-5. GitHub ActionsでCI化する
    6. 8-6. WebPageTestのAPIから走らせる
  9. 9. RUM(Real User Monitoring)— 実ユーザー計測
    1. 9-1. web-vitalsをsendBeaconでバックエンドへ送る
    2. 9-2. Sentry Performance連携
    3. 9-3. DataDog RUM導入
    4. 9-4. Google Analytics 4にWeb Vitalsを送る
    5. 9-5. Service Worker経由でRUMをまとめて送る
    6. 9-6. クライアント側からSWへmetricを送る
  10. 10. Node.js のパフォーマンス計測
    1. 10-1. perf_hooks の基本
    2. 10-2. Node.jsのEvent Loop遅延を計測する
    3. 10-3. clinic.js Doctor で総合診断
    4. 10-4. clinic flame でフレームグラフを取る
    5. 10-5. autocannon で負荷をかけながら計測
    6. 10-6. –prof で V8プロファイルを取る
  11. 11. OpenTelemetry — 分散トレーシング
    1. 11-1. ブラウザのオートインストルメンテーション
    2. 11-2. Node.js側でtraceを受け取る
    3. 11-3. 手動spanで業務処理を計測
  12. 12. CI/CDに組み込む実践パターン
    1. 12-1. パフォーマンスバジェットを設定する
    2. 12-2. CIで bundlesize / size-limit を使う
    3. 12-3. PRごとに数値差分をコメントする
    4. 12-4. 退行検知のしきい値設計
  13. 13. 計測データを「行動」につなげるためのチェックリスト
    1. 13-1. 計測する前に決めるべき5つ
    2. 13-2. 改善後に「再計測してから喜ぶ」
    3. 13-3. ダッシュボードに「劣化トレンド」を出す
  14. 14. まとめ — 「測れない最適化」を卒業しよう
  15. 15. パフォーマンス学習をキャリアに変える

1. JavaScriptパフォーマンス計測の全体像

1-1. 「計測しない最適化」は害悪である

パフォーマンスチューニングで最も多い失敗は「体感で速くなった気がする」だけで終わることです。実際には変わっていない、あるいは別の場所が遅くなっているケースが半分以上を占めます。まずは計測→ボトルネック特定→改善→再計測のサイクルを徹底しましょう。

1-2. 計測の3レイヤー(マイクロ・ページ・フィールド)

  • マイクロベンチマーク: 関数単位の速度比較(Benchmark.js / tinybench / mitata)
  • ページパフォーマンス: 1ページの読み込み・描画・操作応答(Performance API / Lighthouse)
  • フィールドデータ: 実ユーザー環境の計測(web-vitals / Sentry / DataDog / CrUX)

3レイヤーを混同するとデータの意味を読み違えます。「Lighthouseで90点なのに実ユーザー体感が悪い」という典型例は、ラボとフィールドのギャップです。

1-3. console.timeから卒業しよう

// 簡易だが粒度が荒くstackable性も弱い
console.time("hoge");
doSomething();
console.timeEnd("hoge"); // hoge: 12.345ms

console.time自体は便利ですが、ミリ秒未満の精度・ネスト・複数計測には不向きです。次節からはより高精度な手段に移ります。

2. Performance API — 高精度タイマーとUser Timing

2-1. performance.now() の基本

// Date.now()よりはるかに高精度(0.005ms単位)で単調増加
const t0 = performance.now();
for (let i = 0; i < 1_000_000; i++) {
  Math.sqrt(i);
}
const t1 = performance.now();
console.log(`elapsed: ${(t1 - t0).toFixed(3)} ms`);

2-2. performance.mark() と performance.measure()

// 名前付きマークで時系列を残せる(DevToolsのPerformanceパネルにも出る)
performance.mark("fetch-start");
await fetch("/api/users");
performance.mark("fetch-end");

performance.measure("fetch-duration", "fetch-start", "fetch-end");

const entries = performance.getEntriesByName("fetch-duration");
console.log(entries[0].duration.toFixed(2), "ms");

2-3. measureUserAgentSpecificMemory(将来用)

// Chrome系で利用可能。crossOriginIsolated必須
if (performance.measureUserAgentSpecificMemory) {
  const result = await performance.measureUserAgentSpecificMemory();
  console.log("bytes:", result.bytes);
}

2-4. PerformanceObserverで非同期に拾う

// markされたエントリを後から束で受け取れる(同期処理を邪魔しない)
const po = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.duration);
  }
});
po.observe({ entryTypes: ["measure"] });

2-5. 関数デコレーター化して計測を自動化

// 任意の関数を「mark/measure付き」にラップするユーティリティ
function withTiming<T extends (...args: any[]) => any>(label: string, fn: T): T {
  return ((...args: Parameters<T>) => {
    performance.mark(`${label}-start`);
    const ret = fn(...args);
    if (ret instanceof Promise) {
      return ret.finally(() => {
        performance.mark(`${label}-end`);
        performance.measure(label, `${label}-start`, `${label}-end`);
      });
    }
    performance.mark(`${label}-end`);
    performance.measure(label, `${label}-start`, `${label}-end`);
    return ret;
  }) as T;
}

const slowAdd = withTiming("slowAdd", (a: number, b: number) => {
  for (let i = 0; i < 1e6; i++) {}
  return a + b;
});
slowAdd(1, 2);

2-6. clearMarks / clearMeasuresで肥大化を防ぐ

// 長時間稼働ページでは溜まり続けるので定期的にクリアする
setInterval(() => {
  performance.clearMarks();
  performance.clearMeasures();
}, 60_000);

3. Web Vitals — ユーザー体感の3指標を測る

3-1. Core Web Vitalsの再整理(LCP / INP / CLS)

  • LCP(Largest Contentful Paint): 主要コンテンツが描画されるまで。目標2.5秒以内
  • INP(Interaction to Next Paint): 操作から次の描画まで。目標200ms以内
  • CLS(Cumulative Layout Shift): 視覚的安定性。目標0.1未満

2024年にFIDがINPに置き換わったため、古い記事のFID計測コードは現在ほぼ無意味です。

3-2. web-vitalsライブラリの導入

npm install web-vitals
// すべてのCore Web Vitalsをまとめて取得
import { onLCP, onINP, onCLS, onTTFB, onFCP } from "web-vitals";

onLCP(console.log);
onINP(console.log);
onCLS(console.log);
onTTFB(console.log);
onFCP(console.log);

3-3. attribution版で「どこが原因か」まで取る

import { onLCP } from "web-vitals/attribution";

onLCP((metric) => {
  // どの要素がLCPを引き起こしたかが分かる
  console.log("LCP element:", metric.attribution.element);
  console.log("URL:", metric.attribution.url);
  console.log("resource load delay:", metric.attribution.resourceLoadDelay);
});

3-4. INPの内訳を分解する

import { onINP } from "web-vitals/attribution";

onINP((metric) => {
  const a = metric.attribution;
  console.log({
    eventType: a.interactionType,
    eventTarget: a.interactionTargetElement,
    inputDelay: a.inputDelay,
    processingDuration: a.processingDuration,
    presentationDelay: a.presentationDelay,
  });
}, { reportAllChanges: true });

3-5. CLSの原因要素を特定

import { onCLS } from "web-vitals/attribution";

onCLS((metric) => {
  for (const entry of metric.attribution.largestShiftSource ? [metric.attribution.largestShiftSource] : []) {
    console.log("shift source:", entry.node);
  }
});

4. PerformanceObserver — ブラウザ内部イベントを購読する

4-1. Navigation Timing(ページ全体の遷移指標)

const nav = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming;
console.log({
  ttfb: nav.responseStart - nav.requestStart,
  domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
  load: nav.loadEventEnd - nav.startTime,
  transferSize: nav.transferSize,
  encodedBodySize: nav.encodedBodySize,
});

4-2. Resource Timing(個別リソースの内訳)

// 遅いリソースTOP10を抽出
const slow = performance
  .getEntriesByType("resource")
  .sort((a, b) => b.duration - a.duration)
  .slice(0, 10);

slow.forEach((r) => console.log(r.name, r.duration.toFixed(1), "ms"));

4-3. Long Tasks API(50ms超のメインスレッド占有)

// レンダリングを止める「長いタスク」を検出する
const lt = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.warn("long task:", entry.duration.toFixed(0), "ms", entry.attribution);
  }
});
lt.observe({ type: "longtask", buffered: true });

4-4. Element Timing(任意要素の描画時刻)

<img src="/hero.jpg" elementtiming="hero-img" />
<h1 elementtiming="hero-heading">高速化を始めよう</h1>
new PerformanceObserver((list) => {
  for (const entry of list.getEntries() as PerformanceElementTiming[]) {
    console.log(entry.identifier, entry.renderTime.toFixed(0), "ms");
  }
}).observe({ type: "element", buffered: true });

4-5. Event Timing(個別イベントの遅延)

new PerformanceObserver((list) => {
  for (const entry of list.getEntries() as PerformanceEventTiming[]) {
    if (entry.duration > 40) {
      console.log("slow event:", entry.name, entry.duration.toFixed(0));
    }
  }
}).observe({ type: "event", buffered: true, durationThreshold: 16 });

4-6. Largest Contentful Paint 単体取得

new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const last = entries[entries.length - 1];
  console.log("LCP:", last.startTime.toFixed(0), "ms", (last as any).element);
}).observe({ type: "largest-contentful-paint", buffered: true });

4-7. paint タイプ(FP / FCP)

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.startTime.toFixed(0), "ms");
  }
}).observe({ type: "paint", buffered: true });

5. Memory計測 — リーク検出とヒープ監視

5-1. performance.memory(Chrome系のみ)

// 標準化前の非標準APIだが実運用デバッグでは便利
// @ts-ignore
const mem = performance.memory;
console.log({
  usedJSHeapSize: mem.usedJSHeapSize,
  totalJSHeapSize: mem.totalJSHeapSize,
  jsHeapSizeLimit: mem.jsHeapSizeLimit,
});

5-2. メモリ増加を時系列ログにする

function logHeap(label: string) {
  // @ts-ignore
  const m = performance.memory;
  if (!m) return;
  console.log(label, (m.usedJSHeapSize / 1024 / 1024).toFixed(1), "MB");
}

setInterval(() => logHeap("tick"), 5000);

5-3. リークパターンを再現するスニペット

// イベントリスナーを外し忘れるとDOMがGCされず溜まり続ける
const leaked: HTMLElement[] = [];
function leak() {
  const div = document.createElement("div");
  document.body.appendChild(div);
  div.addEventListener("click", () => console.log(div));
  leaked.push(div); // GC対象にならない
}
setInterval(leak, 100);

5-4. WeakRef / FinalizationRegistryでGCを観察

const registry = new FinalizationRegistry((label) => {
  console.log("GCされた:", label);
});

let obj: any = { big: new Array(1e6).fill(0) };
registry.register(obj, "obj");
obj = null; // しばらくするとログが出る(GCタイミングは保証されない)

6. マイクロベンチマーク — 関数単位の速度比較

6-1. Benchmark.jsの基本

npm install benchmark
import Benchmark from "benchmark";

const suite = new Benchmark.Suite();

suite
  .add("for loop", () => {
    let s = 0;
    for (let i = 0; i < 1000; i++) s += i;
  })
  .add("reduce", () => {
    Array.from({ length: 1000 }, (_, i) => i).reduce((a, b) => a + b, 0);
  })
  .on("cycle", (e: any) => console.log(String(e.target)))
  .on("complete", function (this: any) {
    console.log("Fastest is " + this.filter("fastest").map("name"));
  })
  .run({ async: true });

6-2. tinybench(モダンな後継)

npm install tinybench
import { Bench } from "tinybench";

const bench = new Bench({ time: 1000 });

bench
  .add("Array.from", () => Array.from({ length: 1000 }, (_, i) => i))
  .add("for push", () => {
    const a: number[] = [];
    for (let i = 0; i < 1000; i++) a.push(i);
  });

await bench.run();
console.table(bench.table());

6-3. mitata(超軽量・高精度)

npm install mitata
import { run, bench, group } from "mitata";

group("string concat", () => {
  bench("+ operator", () => {
    let s = "";
    for (let i = 0; i < 1000; i++) s += "a";
    return s;
  });
  bench("Array.join", () => {
    const a: string[] = [];
    for (let i = 0; i < 1000; i++) a.push("a");
    return a.join("");
  });
});

await run();

6-4. ベンチマークの「嘘」を避けるコツ

// JITがdead-code eliminationしないように戻り値を使う
import { bench, run } from "mitata";

let sink: number;
bench("compute", () => {
  let s = 0;
  for (let i = 0; i < 1e4; i++) s += Math.sqrt(i);
  sink = s; // 副作用を残してDCEを防ぐ
});

await run();
console.log(sink);

7. Chrome DevTools Performance Profiler

7-1. 録画の基本フロー

  1. DevToolsを開き Performance タブへ
  2. 左上の録画ボタン(または Ctrl+E)
  3. 計測したい操作を実行(5〜15秒推奨)
  4. 録画停止→Flame Chartで重い関数を探す

7-2. console.profile() でコードから録画開始

// 関数内の特定区間だけプロファイルを取れる
console.profile("heavyTask");
runHeavyTask();
console.profileEnd("heavyTask");

7-3. CPU Profile JSON の出力と解析

// Node.jsの場合: --cpu-prof フラグでJSON出力できる
// node --cpu-prof --cpu-prof-name=app.cpuprofile app.js
// 出力された .cpuprofile を Chrome DevTools の Performance タブにドロップすれば閲覧可能

7-4. Performance.profileを動的に取る(–inspect)

// Node.jsを --inspect で起動し、chrome://inspect から接続
// Profilerタブで Start → 操作 → Stop で .cpuprofile が得られる

7-5. Memory Snapshot(ヒープスナップショット)

DevTools の Memory タブで Heap snapshot を取得→Comparison ビューで「2回スナップショット間に増えたオブジェクト」を確認するのがリーク追跡の王道です。

7-6. Allocation Profile(タイムライン上の確保)

Memoryタブの Allocation instrumentation on timeline で、いつどんなオブジェクトが確保されたかを時系列で追えます。短時間に大量の小オブジェクトが確保される箇所がGC圧の原因です。

7-7. Coverage タブで未使用コードを発見

DevTools の Coverage タブを開き、ページをリロードすると JS/CSS の使用率が表示されます。初回ロードで50%以上使われていないファイルはコード分割の候補です。

8. Lighthouse / Lighthouse CI

8-1. Lighthouse CLIインストールと実行

npm install -g lighthouse
lighthouse https://example.com --view --output=html --output-path=./report.html

8-2. JSONで構造化結果を保存

lighthouse https://example.com 
  --output=json --output-path=./lh.json 
  --only-categories=performance 
  --chrome-flags="--headless"

8-3. プログラムから呼ぶ

import lighthouse from "lighthouse";
import { launch } from "chrome-launcher";

const chrome = await launch({ chromeFlags: ["--headless"] });
const result = await lighthouse("https://example.com", {
  port: chrome.port,
  output: "json",
  onlyCategories: ["performance"],
});
console.log("score:", result.lhr.categories.performance.score * 100);
await chrome.kill();

8-4. Lighthouse CIで予算管理(lighthouserc.js)

module.exports = {
  ci: {
    collect: {
      url: ["https://example.com/"],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        "categories:performance": ["error", { minScore: 0.9 }],
        "largest-contentful-paint": ["warn", { maxNumericValue: 2500 }],
        "interactive": ["error", { maxNumericValue: 3500 }],
      },
    },
    upload: { target: "temporary-public-storage" },
  },
};

8-5. GitHub ActionsでCI化する

name: Lighthouse CI
on: [push]
jobs:
  lhci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run build
      - run: npx @lhci/cli@0.13.x autorun

8-6. WebPageTestのAPIから走らせる

// WPTはCrUXより細かい waterfall を返す
const res = await fetch(
  `https://www.webpagetest.org/runtest.php?url=${encodeURIComponent("https://example.com")}&k=${API_KEY}&f=json`
);
const data = await res.json();
console.log(data.data.userUrl);

9. RUM(Real User Monitoring)— 実ユーザー計測

9-1. web-vitalsをsendBeaconでバックエンドへ送る

import { onLCP, onINP, onCLS } from "web-vitals";

function send(metric: any) {
  const body = JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, url: location.pathname });
  // sendBeaconは離脱時でも確実に飛ぶ
  navigator.sendBeacon("/api/rum", body);
}

onLCP(send);
onINP(send);
onCLS(send);

9-2. Sentry Performance連携

import * as Sentry from "@sentry/browser";
import { browserTracingIntegration } from "@sentry/browser";

Sentry.init({
  dsn: "https://xxx@oXXXX.ingest.sentry.io/XXXX",
  integrations: [browserTracingIntegration()],
  tracesSampleRate: 0.2, // 20%サンプリング
});

// 任意処理を計測
Sentry.startSpan({ name: "checkout", op: "ui.action" }, async () => {
  await runCheckout();
});

9-3. DataDog RUM導入

import { datadogRum } from "@datadog/browser-rum";

datadogRum.init({
  applicationId: "xxxx",
  clientToken: "pubxxxx",
  site: "datadoghq.com",
  service: "my-app",
  sessionSampleRate: 100,
  sessionReplaySampleRate: 10,
  trackUserInteractions: true,
});

datadogRum.startSessionReplayRecording();

9-4. Google Analytics 4にWeb Vitalsを送る

import { onLCP, onINP, onCLS } from "web-vitals";

function sendToGA4(metric: any) {
  // gtagはGA4のグローバル関数
  // @ts-ignore
  window.gtag?.("event", metric.name, {
    value: Math.round(metric.name === "CLS" ? metric.value * 1000 : metric.value),
    metric_id: metric.id,
    metric_value: metric.value,
    metric_delta: metric.delta,
    metric_rating: metric.rating,
  });
}

onLCP(sendToGA4);
onINP(sendToGA4);
onCLS(sendToGA4);

9-5. Service Worker経由でRUMをまとめて送る

// SWでバッチングするとビーコン回数を減らせる
self.addEventListener("message", (e) => {
  if (e.data?.type === "rum") {
    // BroadcastChannelやIndexedDBに蓄積→定期flush
    queueRum(e.data.payload);
  }
});

async function flush() {
  const batch = await drainQueue();
  if (batch.length) {
    await fetch("/api/rum/batch", { method: "POST", body: JSON.stringify(batch), keepalive: true });
  }
}
setInterval(flush, 10_000);

9-6. クライアント側からSWへmetricを送る

import { onINP } from "web-vitals";

onINP((m) => {
  navigator.serviceWorker.controller?.postMessage({
    type: "rum",
    payload: { name: m.name, value: m.value, url: location.pathname, t: Date.now() },
  });
});

10. Node.js のパフォーマンス計測

10-1. perf_hooks の基本

import { performance, PerformanceObserver } from "node:perf_hooks";

const obs = new PerformanceObserver((items) => {
  for (const e of items.getEntries()) {
    console.log(e.name, e.duration.toFixed(2), "ms");
  }
});
obs.observe({ entryTypes: ["measure"] });

performance.mark("a");
await new Promise((r) => setTimeout(r, 100));
performance.mark("b");
performance.measure("a-to-b", "a", "b");

10-2. Node.jsのEvent Loop遅延を計測する

import { monitorEventLoopDelay } from "node:perf_hooks";

const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();

setInterval(() => {
  console.log({
    min: h.min / 1e6,
    mean: h.mean / 1e6,
    max: h.max / 1e6,
    p99: h.percentile(99) / 1e6,
  });
  h.reset();
}, 5000);

10-3. clinic.js Doctor で総合診断

npm install -g clinic
clinic doctor -- node server.js
# 終了後ブラウザでレポートが開き、ボトルネックの種類(CPU/IO/GC/EventLoop)を分類してくれる

10-4. clinic flame でフレームグラフを取る

clinic flame -- node server.js

10-5. autocannon で負荷をかけながら計測

npm install -g autocannon
autocannon -c 100 -d 30 http://localhost:3000/

10-6. –prof で V8プロファイルを取る

node --prof app.js
# 完了後 isolate-*.log が生成される
node --prof-process isolate-*.log > processed.txt

11. OpenTelemetry — 分散トレーシング

11-1. ブラウザのオートインストルメンテーション

npm install @opentelemetry/sdk-trace-web @opentelemetry/instrumentation-fetch
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";

const provider = new WebTracerProvider();
provider.register();

registerInstrumentations({
  instrumentations: [new FetchInstrumentation({ propagateTraceHeaderCorsUrls: [/.+/] })],
});

11-2. Node.js側でtraceを受け取る

import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";

const sdk = new NodeSDK({
  instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();

11-3. 手動spanで業務処理を計測

import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("checkout");

await tracer.startActiveSpan("validate-cart", async (span) => {
  try {
    await validateCart();
  } finally {
    span.end();
  }
});

12. CI/CDに組み込む実践パターン

12-1. パフォーマンスバジェットを設定する

// budget.json
[
  {
    "path": "/*",
    "resourceSizes": [
      { "resourceType": "script", "budget": 300 },
      { "resourceType": "stylesheet", "budget": 50 },
      { "resourceType": "image", "budget": 500 }
    ],
    "timings": [
      { "metric": "interactive", "budget": 3500 },
      { "metric": "first-contentful-paint", "budget": 1800 }
    ]
  }
]

12-2. CIで bundlesize / size-limit を使う

npm install --save-dev size-limit @size-limit/preset-app
// package.json
{
  "size-limit": [
    { "path": "dist/main.*.js", "limit": "200 KB" }
  ],
  "scripts": { "size": "size-limit" }
}

12-3. PRごとに数値差分をコメントする

// GitHub Actionsでサイズ差分をPRコメント
- uses: andresz1/size-limit-action@v1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}

12-4. 退行検知のしきい値設計

本番ベンチマークのp75を基準に「5%以上の悪化でwarn・10%以上でfail」と段階を分けると、ノイズによる過剰アラートを避けつつ退行を逃しません。

13. 計測データを「行動」につなげるためのチェックリスト

13-1. 計測する前に決めるべき5つ

  • 計測対象のユーザー操作(ログイン、カート追加 など)
  • 計測する指標(LCP・INP・カスタムmark・APIレイテンシ)
  • 計測の粒度(p50/p75/p95のどれを基準にするか)
  • 許容できる退行幅(例: p75で+50ms以内)
  • 退行時の巻き戻し方針(自動revert / manual roll-out)

13-2. 改善後に「再計測してから喜ぶ」

改善コミットの直後に必ず同じシナリオで再計測し、想定通りの差分が出ているかを確認します。Lighthouseは実行ごとに揺れるため、3回平均を基本にしましょう。

13-3. ダッシュボードに「劣化トレンド」を出す

単発のスコアではなく、過去30日のp75トレンドを可視化すると、リリースとの相関やじわじわ悪化するパターンが見えるようになります。

14. まとめ — 「測れない最適化」を卒業しよう

JavaScriptのパフォーマンス計測は、マイクロ(関数)・ページ・フィールドの3レイヤーを使い分けることが最重要です。

  • 関数単位は tinybench / mitata / Benchmark.js
  • ページ単位は Performance API + PerformanceObserver + Lighthouse
  • フィールドは web-vitals + Sentry / DataDog / GA4 / OpenTelemetry
  • Node.jsは perf_hooks + clinic.js + –prof
  • CIでは Lighthouse CI + size-limit + パフォーマンスバジェット

すべてを最初から導入する必要はありません。まずはweb-vitalsをGA4に送るところから始め、ダッシュボードで「何が遅いか」を見えるようにしましょう。そこから初めて、本記事のProfilerやベンチマークが「効く」ようになります。「測れない最適化は祈り」を脱して、数値で語れるエンジニアになっていきましょう。

15. パフォーマンス学習をキャリアに変える

パフォーマンス計測・改善のスキルは、フロントエンドエンジニアの中でも特に評価が高く、年収アップやリードエンジニア登用の決め手になります。独学が難しいと感じたら、現役エンジニアによるメンタリング付きスクールで体系的に学ぶのが最短ルートです。

  • テックアカデミー: 短期集中でフロントエンド+パフォーマンスチューニングを学べる。週2回のメンタリングで疑問即解決。
  • 侍エンジニア: 完全マンツーマン。LighthouseやWeb Vitalsをポートフォリオ案件で実装まで指導してもらえる。
  • DMM WEBCAMP: 転職保証付き。実務想定のチーム開発で計測→改善のサイクルを経験できる。
  • レバテックキャリア(現役向け): パフォーマンス改善実績を年収交渉に直結させたい方に。フロントの上位案件・年収700万〜1200万のオファーが多数。

「測れる人」は、現場で圧倒的に重宝されます。本記事のコードを手元で1つずつ動かしながら、ぜひ次のキャリアステップへ繋げてください。

コメント

タイトルとURLをコピーしました