パフォーマンス計測 コピペで使える Tips 集【実務向け】

パフォーマンス計測 コピペで使えるコードを一か所にまとめました。「計測できないものは改善できない」を前提に、ブラウザ・Node.js・CI まで幅広くカバーしています。

performance.now() / performance.mark() で処理時間を計測する

performance.now() で任意区間を計測

const start = performance.now();

// 計測したい処理
await heavyTask();

const elapsed = performance.now() - start;
console.log(`${elapsed.toFixed(2)} ms`);

performance.mark() と measure() でタイムラインに残す

DevTools の Performance タブで可視化できるのが利点です。

performance.mark("parse:start");
const data = JSON.parse(largeJson);
performance.mark("parse:end");

performance.measure("parse", "parse:start", "parse:end");

const [entry] = performance.getEntriesByName("parse");
console.log(`${entry.duration.toFixed(2)} ms`);

performance.clearMarks();
performance.clearMeasures();

ラッパー関数で使い回す

async function measure<T>(label: string, fn: () => Promise<T>): Promise<T> {
  performance.mark(`${label}:start`);
  const result = await fn();
  performance.mark(`${label}:end`);
  performance.measure(label, `${label}:start`, `${label}:end`);
  const [entry] = performance.getEntriesByName(label);
  console.log(`[${label}] ${entry.duration.toFixed(2)} ms`);
  return result;
}

// 使い方
const users = await measure("fetchUsers", () => fetchUsers());

console.time / timeEnd の使い分け

console.time は手軽ですが、performance.now() より精度・用途が限られます。使い分けの基準は以下のとおりです。

用途推奨
手軽にデバッグconsole.time
精度が必要・DevTools に残したいperformance.mark
Node.js スクリプトprocess.hrtime.bigint()
console.time("label");
doSomething();
console.timeEnd("label");
// label: 12.345ms

ネストしたラベルで複数区間を同時計測できます。

console.time("total");
  console.time("step1");
  await step1();
  console.timeEnd("step1");

  console.time("step2");
  await step2();
  console.timeEnd("step2");
console.timeEnd("total");

Node.js で高精度な計測が必要な場合は process.hrtime.bigint() を使います。

const start = process.hrtime.bigint();
doHeavyWork();
const end = process.hrtime.bigint();
console.log(`${Number(end - start) / 1_000_000} ms`);

Lighthouse CLI でスコアを自動計測する

インストールと基本実行

npm install -g lighthouse
lighthouse https://example.com --output json --output-path ./report.json --chrome-flags="--headless"

スコアだけ取り出す

lighthouse https://example.com \
  --output json \
  --output-path ./lh.json \
  --chrome-flags="--headless" \
  --quiet

node -e "
const r = require('./lh.json');
const cats = r.categories;
console.log('Performance:', Math.round(cats.performance.score * 100));
console.log('Accessibility:', Math.round(cats.accessibility.score * 100));
console.log('Best Practices:', Math.round(cats['best-practices'].score * 100));
console.log('SEO:', Math.round(cats.seo.score * 100));
"

CI(GitHub Actions)に組み込む

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
  lhci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci && npm run build
      - run: npx lhci autorun

lighthouserc.json でスコアの下限を設定してゲートにできます。

{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["warn", { "minScore": 0.9 }]
      }
    },
    "upload": { "target": "temporary-public-storage" }
  }
}

Bundle サイズを分析する

Vite: vite-bundle-visualizer

npx vite-bundle-visualizer

vite.config.ts でビルド後に自動実行したい場合は以下のとおりです。

import { visualizer } from "rollup-plugin-visualizer";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      filename: "dist/stats.html",
      gzipSize: true,
      brotliSize: true,
    }),
  ],
});

webpack: webpack-bundle-analyzer

npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "static",
      reportFilename: "bundle-report.html",
      openAnalyzer: false,
    }),
  ],
};
npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json

サイズを数値で確認する(CI 向け)

# ビルド後の JS/CSS 合計サイズを確認
find dist -name "*.js" -o -name "*.css" | xargs du -sh | sort -h

# gzip 後のサイズ
find dist/assets -name "*.js" | while read f; do
  original=$(wc -c < "$f")
  gzipped=$(gzip -c "$f" | wc -c)
  echo "$f: ${original}B → ${gzipped}B (gzip)"
done

Web Vitals(LCP・CLS・INP)を計測する

web-vitals ライブラリで計測

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

onLCP((metric) => {
  console.log("LCP:", metric.value, "ms");
});

onCLS((metric) => {
  console.log("CLS:", metric.value);
});

onINP((metric) => {
  console.log("INP:", metric.value, "ms");
});

Analytics に送信する

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

function sendToAnalytics(metric: Metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    id: metric.id,
    navigationType: metric.navigationType,
  });

  navigator.sendBeacon("/api/vitals", body);
}

onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
onINP(sendToAnalytics);

ブラウザ DevTools で手動確認(コピペ用スニペット)

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

new PerformanceObserver((list) => {
  let clsScore = 0;
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) clsScore += entry.value;
  }
  console.log("CLS:", clsScore.toFixed(4));
}).observe({ type: "layout-shift", buffered: true });

Node.js の —prof と clinic.js でプロファイリングする

—prof でフレームグラフを生成

node --prof server.js

# 負荷をかけてから Ctrl+C で停止
# isolate-*.log が生成される
node --prof-process isolate-*.log > processed.txt
cat processed.txt | head -60

clinic.js でビジュアルプロファイリング

npm install -g clinic
# フレームグラフ(CPU ボトルネック)
clinic flame -- node server.js

# バブルチャート(I/O・イベントループ)
clinic bubbleprof -- node server.js

# ヒープ解析(メモリリーク)
clinic heapprofiler -- node server.js

単発の関数をプロファイルする

import { Session } from "node:inspector/promises";
import { writeFileSync } from "node:fs";

async function profileFn<T>(fn: () => Promise<T>, outPath: string): Promise<T> {
  const session = new Session();
  session.connect();

  await session.post("Profiler.enable");
  await session.post("Profiler.start");

  const result = await fn();

  const { profile } = await session.post("Profiler.stop");
  writeFileSync(outPath, JSON.stringify(profile));
  session.disconnect();

  console.log(`CPU profile saved to ${outPath}`);
  return result;
}

// 使い方(出力は Chrome DevTools の JavaScript Profiler で開く)
await profileFn(() => heavyComputation(), "profile.cpuprofile");

まとめ

計測対象ツール・API特徴
任意コードの処理時間performance.now() / performance.mark()ブラウザ・Node.js 両対応、DevTools と連携
手軽なデバッグ計測console.time / process.hrtime.bigint()コードが少ない、精度はやや低い
ページ総合スコアLighthouse CLI / LHCICI に組み込んでゲートにできる
バンドルサイズ分析vite-bundle-visualizer / webpack-bundle-analyzerビジュアルでチャンク構成を把握
Core Web Vitalsweb-vitals ライブラリ / PerformanceObserverLCP・CLS・INP をリアルユーザーで計測
Node.js CPU プロファイル--prof / clinic.jsフレームグラフでホットスポットを特定

計測は一度やって終わりではなく、コードの変更や依存関係の更新のたびに継続するのが効果的です。CI に組み込んでリグレッションを自動検知できると、パフォーマンス品質を長期的に保ちやすくなります。