js-performance-patterns
フレームワークに依存しないJavaScriptのランタイムパフォーマンス最適化パターンを提供します。ホットパスやループ、DOM操作、キャッシュ処理、データ構造の選択など、パフォーマンスが重要なコードの最適化が必要なときに使用してください。
description の原文を見る
Provides framework-agnostic JavaScript runtime performance patterns. Use when optimizing hot paths, loops, DOM operations, caching, or data structure choices in performance-critical code.
SKILL.md 本文
JavaScript パフォーマンスパターン
目次
JavaScript のホットパスに対するランタイムパフォーマンスマイクロパターン。これらのパターンは特にタイトループ、頻繁なコールバック (スクロール、リサイズ、アニメーションフレーム)、データ集約的な操作で重要です。React、Vue、バニラ JavaScript、Node.js など、あらゆる JavaScript 環境に適用できます。
使用する場面
以下の場合にこれらのパターンを参照してください:
- プロファイリングでホット関数またはタイトループが明らかになった場合
- 大規模なデータセット (1,000 項目以上) を処理する場合
- 高頻度イベント (スクロール、mousemove、リサイズ) を処理する場合
- ビルド時またはサーバー側スクリプトのパフォーマンスを最適化する場合
- クリティカルパスのコードをパフォーマンスの観点でレビューする場合
手順
- これらのパターンは 計測済みホットパス のみに適用してください。頻繁に実行されるか、大規模なデータセットを処理するコードです。ナノ秒単位のゲインよりも可読性が重要なコールドパスには適用しないでください。
詳細
概要
マイクロ最適化は、アルゴリズム改善の代替品 ではありません。まずアルゴリズムに対応してください (O(n^2) から O(n) へ、ウォーターフォールの削除、DOM 変更の削減)。アルゴリズムが正しい状態になってから、これらのパターンでホットパスから追加のパフォーマンスを引き出してください。
1. ルックアップに Set と Map を使用
影響: 大規模コレクションでは HIGH — ルックアップあたり O(1) vs O(n)。
.includes()、.find()、.indexOf() などの配列メソッドは線形スキャンです。同じコレクションに対して何度もルックアップを繰り返す場合は、まず Set または Map に変換してください。
避けるべき — チェックあたり O(n):
const allowedIds = ['a', 'b', 'c', /* ...hundreds more */]
function isAllowed(id: string) {
return allowedIds.includes(id) // 配列全体をスキャン
}
items.filter(item => allowedIds.includes(item.id)) // O(n * m)
推奨 — チェックあたり 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 を使用してください:
// 避けるべき
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
const user = users.find(u => u.id === targetId) // O(n)
// 推奨
const userMap = new Map(users.map(u => [u.id, u]))
const user = userMap.get(targetId) // O(1)
2. DOM の読み取りと書き込みをバッチ処理
影響: HIGH — レイアウトスラッシングを防止。
DOM 読み取り (例: offsetHeight、getBoundingClientRect) を DOM 書き込み (例: style.height = ...) と交互に行うと、ブラウザはレイアウトを複数回再計算するよう強制されます。すべての読み取りを先に行い、次にすべての書き込みを行ってください。
避けるべき — レイアウトスラッシング (読み取り/書き込み/読み取り/書き込み):
elements.forEach(el => {
const height = el.offsetHeight // 読み取り → レイアウトを強制
el.style.height = `${height * 2}px` // 書き込み
})
// 各イテレーションでレイアウト再計算が強制される
推奨 — バッチ読み取り次に書き込み:
// 読み取りフェーズ
const heights = elements.map(el => el.offsetHeight)
// 書き込みフェーズ
elements.forEach((el, i) => {
el.style.height = `${heights[i] * 2}px`
})
複雑なケースでは、requestAnimationFrame を使用して書き込みを次のフレームに遅延させるか、fastdom のようなライブラリを使用してください。
CSS クラスアプローチ — 単一リフロー:
// 複数のスタイル変更を避ける
el.style.width = '100px'
el.style.height = '200px'
el.style.margin = '10px'
// 推奨 — 1 つのリフロー
el.classList.add('expanded')
// または
el.style.cssText = 'width:100px;height:200px;margin:10px;'
3. タイトループ内のプロパティアクセスをキャッシュ
影響: MEDIUM — 繰り返されるプロパティ解決を削減。
深くネストされたプロパティまたは配列 .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. 高い関数の結果をメモ化
影響: MEDIUM-HIGH — 同じ結果の再計算を回避。
純粋関数が同じ引数で何度も呼び出されるとき、結果をキャッシュしてください。
シンプルな単一値キャッシュ:
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. 同じデータのイテレーションを結合
影響: MEDIUM — 複数ではなく単一パス。
.filter().map().reduce() をチェーンすると中間配列が作成され、データが複数回イテレートされます。ホットパス内の大規模配列の場合は、単一ループに結合してください。
避けるべき — 3 つのイテレーション、2 つの中間配列:
const result = users
.filter(u => u.active)
.map(u => u.name)
.reduce((acc, name) => acc + name + ', ', '')
推奨 — 単一パス:
let result = ''
for (const u of users) {
if (u.active) {
result += u.name + ', '
}
}
小さな配列 (100 項目未満) の場合は、チェーンされたバージョンで問題なく、より読みやすいです。プロファイリングで重要性が示される場合にのみ最適化してください。
6. 長さチェックで最初にショートサーキット
影響: LOW-MEDIUM — 空の入力に対する高い操作を回避。
高い比較または変換を実行する前に、入力が空かどうかを確認してください。
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. 不要な作業をスキップするために早期リターン
影響: LOW-MEDIUM — 平均ケースの実行を削減。
一般的な非マッチングケースの場合、できるだけ早く終了するように関数を構造化してください。
避けるべき — 常に完全な作業を行う:
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 と定数作成をループ外に昇格
影響: LOW-MEDIUM — 繰り返されるコンパイルを回避。
ループ内または頻繁に呼び出される関数内に 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)
})
}
推奨 — 1 回コンパイル:
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))
}
9. イミュータビリティに toSorted()、toReversed()、toSpliced() を使用
影響: LOW — 手動コピーなしで正しいイミュータビリティ。
新しい非変更配列メソッドは [...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)
これらはすべての最新ブラウザと Node.js 20+ で利用可能です。
10. ビジュアル更新に requestAnimationFrame を使用
影響: MEDIUM — ブラウザのレンダリングサイクルと同期。
レンダリングサイクル外 (タイマー、イベントハンドラーなど) からトリガーされた 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 を使用
影響: LOW — ライブラリなしで正しいディープクローン。
structuredClone() は循環参照、型付き配列、Date、RegExp、Map、Set を処理します。これは JSON.parse(JSON.stringify()) とは異なります。
// 避けるべき — Date、Map、Set、undefined 値を失う
const copy = JSON.parse(JSON.stringify(original))
// 推奨 — すべての標準型を処理
const copy = structuredClone(original)
注: structuredClone は関数または DOM ノードをクローンできません。これらのケースでは、カスタムクローンを実装してください。
12. 動的キーに対して Plain Objects ではなく Map を推奨
影響: LOW-MEDIUM — 頻繁な追加/削除でより優れたパフォーマンス。
V8 は静的シェイプの Plain Objects を最適化します。キーが動的に追加および削除される場合 (キャッシュ、カウンター、レジストリ)、Map は一貫してより優れたパフォーマンスを提供します。
// 動的キーに対して避けるべき
const counts: Record<string, number> = {}
items.forEach(item => {
counts[item.category] = (counts[item.category] || 0) + 1
})
// 動的キーに対して推奨
const counts = new Map<string, number>()
items.forEach(item => {
counts.set(item.category, (counts.get(item.category) ?? 0) + 1)
})
出典
patterns.dev からのパターン — ウェブエンジニアリングコミュニティ向けの JavaScript パフォーマンスガイダンス。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- patternsdev
- リポジトリ
- patternsdev/skills
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/patternsdev/skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。