js-performance-patterns について
JavaScript 開発者向けに、測定で確認したホットパスの最適化パターンをまとめたスキルです。ループや検索、DOM 操作などを扱い、まず計測し、重要でないコードでは可読性を保つ方針です。
主な機能
- 01Set と Map による検索
- 02DOM の読み書きを一括化
- 03タイトなループで値をキャッシュ
- 04繰り返す計算のメモ化
- 05データ処理の反復を統合
- 06requestAnimationFrame による画面更新
活用シーン
- 01大規模コレクションでの反復検索の改善
- 02DOM 更新によるレイアウト再計算の削減
- 03高頻度イベントハンドラーの最適化
- 04計測済み JavaScript ホットパスのレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill js-performance-patternsインストール後、Claude Code で/js-performance-patternsを入力して読み込まれたか確認します。
詳細
JavaScript パフォーマンスパターン
目次
JavaScript のホットパス向けランタイムパフォーマンスのマイクロパターンです。これらのパターンは、タイトなループ、高頻度で呼び出されるコールバック(スクロール、リサイズ、アニメーションフレーム)、データ量の多い処理で特に重要です。React、Vue、バニラ JavaScript、Node.js など、あらゆる JavaScript 環境に適用できます。
使用する場面
次のような場合に、これらのパターンを参考にしてください。
- プロファイリングでホット関数やタイトなループが見つかったとき
- 大規模なデータセット(1,000 件以上)を処理するとき
- 高頻度イベント(スクロール、mousemove、リサイズ)を処理するとき
- ビルド時またはサーバーサイドのスクリプトを最適化するとき
- 重要な処理経路のパフォーマンスを考慮してコードをレビューするとき
手順
- これらのパターンは、測定でホットパスだと確認できた箇所、つまり頻繁に実行されるコードや大規模なデータセットを処理するコードにのみ適用してください。ナノ秒単位の向上よりも可読性が重要なコールドパスには適用しないでください。
詳細
概要
マイクロ最適化は、アルゴリズムの改善の代わりにはなりません。まずアルゴリズムを改善してください(O(n^2) から O(n) への変更、ウォーターフォールの除去、DOM 変更の削減)。アルゴリズムを適切にしたうえで、これらのパターンを使ってホットパスのパフォーマンスをさらに引き出します。
1. 検索には Set と Map を使う
効果:大規模なコレクションで高い — 検索 1 回あたり O(1) 対 O(n) です。
.includes()、.find()、.indexOf() などの配列メソッドは、線形に走査します。同じコレクションに対して繰り返し検索する場合は、まず Set または Map に変換してください。
避ける — チェック 1 回あたり O(n):
const allowedIds = ['a', 'b', 'c', /* ...hundreds more */]
function isAllowed(id: string) {
return allowedIds.includes(id) // scans entire array
}
items.filter(item => allowedIds.includes(item.id)) // O(n * m)
推奨 — チェック 1 回あたり O(1):
const allowedIds = new Set(['a', 'b', 'c', /* ...hundreds more */])
function isAllowed(id: string) {
return allowedIds.has(id)
}
items.filter(item => allowedIds.has(item.id)) // O(n)
キーと値の検索には、オブジェクト配列を走査する代わりに Map を使います。
// Avoid
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
const user = users.find(u => u.id === targetId) // O(n)
// Prefer
const userMap = new Map(users.map(u => [u.id, u]))
const user = userMap.get(targetId) // O(1)
2. DOM の読み取りと書き込みをまとめる
効果:高い — レイアウトのスラッシングを防ぎます。
DOM の読み取り(例:offsetHeight、getBoundingClientRect)と DOM の書き込み(例:style.height = ...)を交互に行うと、ブラウザーはレイアウトを何度も再計算することになります。読み取りをすべて先に行い、その後に書き込みをまとめて行ってください。
避ける — レイアウトのスラッシング(読み取り/書き込みを交互に行う):
elements.forEach(el => {
const height = el.offsetHeight // read → forces layout
el.style.height = `${height * 2}px` // write
})
// Each iteration forces a layout recalculation
推奨 — 読み取りをまとめた後に書き込む:
// Read phase
const heights = elements.map(el => el.offsetHeight)
// Write phase
elements.forEach((el, i) => {
el.style.height = `${heights[i] * 2}px`
})
複雑なケースでは、requestAnimationFrame を使って書き込みを次のフレームまで遅らせるか、fastdom などのライブラリを使ってください。
CSS クラスを使う方法 — リフローを 1 回にする:
// Avoid multiple style mutations
el.style.width = '100px'
el.style.height = '200px'
el.style.margin = '10px'
// Prefer — one reflow
el.classList.add('expanded')
// or
el.style.cssText = 'width:100px;height:200px;margin:10px;'
3. タイトなループでプロパティアクセスをキャッシュする
効果:中程度 — プロパティの解決を繰り返す回数を減らします。
タイトなループで、ネストの深いプロパティや配列の .length に毎回アクセスすると、オーバーヘッドが増えます。
避ける:
for (let i = 0; i < data.items.length; i++) {
process(data.items[i].value.nested.prop)
}
推奨:
const { items } = data
for (let i = 0, len = items.length; i < len; i++) {
const val = items[i].value.nested.prop
process(val)
}
これは、要素数が 10,000 件を超える配列や、60fps で呼び出される場合に重要です。小さな配列や呼び出し頻度が低い場合は、読みやすい実装で問題ありません。
4. 高コストな関数の結果をメモ化する
効果:中〜高 — 同じ結果の再計算を避けます。
純粋関数が同じ引数で繰り返し呼び出される場合は、結果をキャッシュしてください。
単一値のシンプルなキャッシュ:
function memoize<T extends (...args: any[]) => any>(fn: T): T {
let lastArgs: any[] | undefined
let lastResult: any
return ((...args: any[]) => {
if (lastArgs && args.every((arg, i) => Object.is(arg, lastArgs![i]))) {
return lastResult
}
lastArgs = args
lastResult = fn(...args)
return lastResult
}) as T
}
const expensiveCalc = memoize((data: number[]) => {
return data.reduce((sum, n) => sum + heavyTransform(n), 0)
})
Map を使った複数キーのキャッシュ:
const cache = new Map<string, Result>()
function getResult(key: string): Result {
if (cache.has(key)) return cache.get(key)!
const result = computeExpensiveResult(key)
cache.set(key, result)
return result
}
上限なく増大する可能性があるキャッシュには、LRU 戦略を使うか、オブジェクトキーには WeakMap を使ってください。
5. 同じデータに対する反復処理をまとめる
効果:中程度 — 複数回の走査を 1 回にします。
.filter().map().reduce() を連結すると、中間配列が作成され、データが複数回走査されます。ホットパスで大きな配列を処理する場合は、単一のループにまとめてください。
避ける — 3 回の走査と 2 つの中間配列:
const result = users
.filter(u => u.active)
.map(u => u.name)
.reduce((acc, name) => acc + name + ', ', '')
推奨 — 1 回の走査:
let result = ''
for (const u of users) {
if (u.active) {
result += u.name + ', '
}
}
小さな配列(100 件未満)では、メソッドを連結する方法で問題なく、より読みやすくなります。プロファイリングで効果が確認できた場合にのみ最適化してください。
6. 最初に長さをチェックして短絡する
効果:低〜中 — 入力が空の場合に高コストな処理を避けます。
高コストな比較や変換を実行する前に、入力が空かどうかをチェックしてください。
function findMatchingItems(items: Item[], query: string): Item[] {
if (items.length === 0 || query.length === 0) return []
const normalized = query.toLowerCase()
return items.filter(item =>
item.name.toLowerCase().includes(normalized)
)
}
7. 不要な処理を省くため早期リターンする
影響度:低〜中 — 平均的なケースでの実行時間を短縮します。
一致しない一般的なケースでは、できるだけ早く終了するように関数を構成します。
避ける — 常にすべての処理を実行します:
function processEvent(event: AppEvent) {
let result = null
if (event.type === 'click') {
if (event.target && event.target.matches('.actionable')) {
result = handleAction(event)
}
}
return result
}
推奨 — 早期に終了します:
function processEvent(event: AppEvent) {
if (event.type !== 'click') return null
if (!event.target?.matches('.actionable')) return null
return handleAction(event)
}
8. RegExp と定数の生成をループの外に移動する
影響度:低〜中 — 繰り返しのコンパイルを回避します。
ループ内や頻繁に呼び出される関数内で RegExp オブジェクトや定数値を生成すると、CPU を浪費します。
避ける — 正規表現を10,000回コンパイルします:
function validate(items: string[]) {
return items.filter(item => {
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return pattern.test(item)
})
}
推奨 — 一度だけコンパイルします:
const EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
function validate(items: string[]) {
return items.filter(item => EMAIL_PATTERN.test(item))
}
これらは、すべての最新ブラウザーと Node.js 20+ で利用できます。
9. 不変性を保つために toSorted()、toReversed()、toSpliced() を使用する
影響度:低 — 手動でコピーすることなく、正しく不変性を保てます。
新しい非破壊的な配列メソッドを使うと、[...arr].sort() のパターンを避けられ、意図もより明確に伝わります。
避ける — 手動でコピーしてから変更します:
const sorted = [...items].sort((a, b) => a.price - b.price)
const reversed = [...items].reverse()
const without = [...items]; without.splice(index, 1)
推奨 — 非破壊的なメソッドを使用します:
const sorted = items.toSorted((a, b) => a.price - b.price)
const reversed = items.toReversed()
const without = items.toSpliced(index, 1)
10. 視覚的な更新には requestAnimationFrame を使用する
影響度:中 — ブラウザーの描画サイクルと同期します。
タイマーやイベントハンドラーなど、描画サイクルの外で実行される DOM の更新は、カクつきを引き起こすことがあります。視覚的な更新は requestAnimationFrame 内でまとめて実行します。
避ける — 描画サイクルの外で更新します:
window.addEventListener('scroll', () => {
progressBar.style.width = `${getScrollPercent()}%`
counter.textContent = `${getScrollPercent()}%`
}, { passive: true })
推奨 — 描画と同期します:
let ticking = false
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
const pct = getScrollPercent()
progressBar.style.width = `${pct}%`
counter.textContent = `${pct}%`
ticking = false
})
ticking = true
}
}, { passive: true })
11. ディープコピーには structuredClone を使用する
影響度:低 — ライブラリを使わずに、正しくディープクローンできます。
structuredClone() は、JSON.parse(JSON.stringify()) とは異なり、循環参照、型付き配列、Date、RegExp、Map、Set を処理できます。
// Avoid — loses Dates, Maps, Sets, undefined values
const copy = JSON.parse(JSON.stringify(original))
// Prefer — handles all standard types
const copy = structuredClone(original)
注:structuredClone では、関数や DOM ノードをクローンできません。そのような場合は、カスタムクローンを実装してください。
12. 動的なキーにはプレーンオブジェクトより Map を優先する
影響度:低〜中 — 頻繁な追加や削除で、より高いパフォーマンスを発揮します。
V8 は、形状が静的なプレーンオブジェクトを最適化します。キーが動的に追加・削除される場合(キャッシュ、カウンター、レジストリーなど)、Map を使うと一貫してより高いパフォーマンスが得られます。
// Avoid for dynamic keys
const counts: Record<string, number> = {}
items.forEach(item => {
counts[item.category] = (counts[item.category] || 0) + 1
})
// Prefer for dynamic keys
const counts = new Map<string, number>()
items.forEach(item => {
counts.set(item.category, (counts.get(item.category) ?? 0) + 1)
})
出典
より広範なウェブエンジニアリングコミュニティ向けの JavaScript パフォーマンスに関するガイダンスである patterns.dev のパターン。