JavaScript パフォーマンス最適化の実践テクニック 2025年版
2025年最新のJavaScriptパフォーマンス最適化テクニックを実例とともに解説。メモリ管理、非同期処理、バンドル最適化まで網羅的にカバーします。
JavaScriptのメモリ管理の仕組みを完全解説。ガベージコレクション、メモリリークの検出方法、Chrome DevToolsでのプロファイリング、実践的な最適化テクニックを詳しく紹介します。
JavaScriptのメモリ管理は、高パフォーマンスな Web アプリケーションを構築する上で極めて重要な要素です。本記事では、JavaScriptエンジンがどのようにメモリを管理し、開発者がどのようにメモリリークを防ぎ、アプリケーションを最適化できるかを包括的に解説します。
JavaScriptにおけるメモリ管理は、主に 3 つのフェーズで構成されています。
変数宣言、オブジェクト生成時に必要なメモリを確保
割り当てられたメモリの読み書き操作
不要になったメモリをガベージコレクションで自動解放
JavaScriptでは、以下のような場面でメモリが割り当てられます:
// プリミティブ値の割り当て(スタックメモリ)
let number = 42; // 8バイト
let string = "Hello World"; // 文字列長に応じたメモリ
let boolean = true; // 1バイト
// オブジェクトの割り当て(ヒープメモリ)
let object = { // オブジェクト全体がヒープに
name: "JavaScript",
version: "ES2025"
};
// 配列の割り当て
let array = [1, 2, 3, 4, 5]; // 要素数に応じたヒープメモリ
// 関数の割り当て
function calculate() { // 関数もヒープに格納
return 42;
}
大量のデータを扱う場合は、一度に大きなメモリを確保するよりも、必要に応じて段階的に割り当てることで、メモリフラグメンテーションを防ぐことができます。
V8 エンジンを例に、モダンな JavaScriptエンジンのガベージコレクション(GC)について詳しく見ていきましょう。
チャートを読み込み中...
V8 では、オブジェクトを「新世代」と「旧世代」に分けて管理します:
世代 | 特徴 | GC頻度 | メモリサイズ |
---|---|---|---|
新世代(Young Generation) | 短命なオブジェクト | 高頻度(数ミリ秒) | 1-8MB |
旧世代(Old Generation) | 長寿命なオブジェクト | 低頻度(数秒) | 数百MB〜数GB |
// グローバルスコープの汚染
function loadUserData() {
// varを使わず宣言 → グローバル変数に
userData = fetchUserFromAPI();
tempData = processData(userData);
}
// イベントリスナーの解除忘れ
element.addEventListener('click', function() {
// 大量のデータ処理
});
// 適切なスコープ管理
function loadUserData() {
const userData = fetchUserFromAPI();
const tempData = processData(userData);
return { userData, tempData };
}
// イベントリスナーの適切な管理
const clickHandler = function() {
// 大量のデータ処理
};
element.addEventListener('click', clickHandler);
// 不要になったら解除
element.removeEventListener('click', clickHandler);
// グローバルスコープの汚染
function loadUserData() {
// varを使わず宣言 → グローバル変数に
userData = fetchUserFromAPI();
tempData = processData(userData);
}
// イベントリスナーの解除忘れ
element.addEventListener('click', function() {
// 大量のデータ処理
});
// 適切なスコープ管理
function loadUserData() {
const userData = fetchUserFromAPI();
const tempData = processData(userData);
return { userData, tempData };
}
// イベントリスナーの適切な管理
const clickHandler = function() {
// 大量のデータ処理
};
element.addEventListener('click', clickHandler);
// 不要になったら解除
element.removeEventListener('click', clickHandler);
// DOM要素とJavaScriptオブジェクトの循環参照
class Component {
constructor(element) {
this.element = element;
// DOM要素からこのインスタンスを参照
element.component = this;
// イベントリスナーでの参照
element.addEventListener('click', () => {
this.handleClick();
});
}
destroy() {
// 循環参照を解除
delete this.element.component;
// イベントリスナーも解除
this.element.removeEventListener('click', this.handleClick);
this.element = null;
}
}
function createDataProcessor() {
const hugeData = new Array(1000000).fill('data');
return function process(item) {
// hugeData全体が保持される
return hugeData.includes(item);
};
}
function createDataProcessor() {
const hugeData = new Array(1000000).fill('data');
// 必要な部分だけを抽出
const dataSet = new Set(hugeData);
return function process(item) {
// Setだけが保持される
return dataSet.has(item);
};
}
function createDataProcessor() {
const hugeData = new Array(1000000).fill('data');
return function process(item) {
// hugeData全体が保持される
return hugeData.includes(item);
};
}
function createDataProcessor() {
const hugeData = new Array(1000000).fill('data');
// 必要な部分だけを抽出
const dataSet = new Set(hugeData);
return function process(item) {
// Setだけが保持される
return dataSet.has(item);
};
}
// メモリリークを検出するためのテストコード
class MemoryLeakTest {
constructor() {
this.data = new Array(10000).fill('test');
this.callbacks = [];
}
addCallback(fn) {
this.callbacks.push(fn);
}
// リークを引き起こすメソッド
createLeak() {
const self = this;
setInterval(() => {
self.data.push(new Date());
}, 100);
}
}
// DevToolsでの分析手順
// 1. Performance タブでレコーディング開始
// 2. メモリ使用量の推移を観察
// 3. Heap Snapshotを複数回取得
// 4. 差分を比較してリークを特定
// スナップショット間の比較
// Snapshot 1: 初期状態
// Snapshot 2: 操作後
// Snapshot 3: GC後
// 差分でリーク対象を特定
// メモリ割り当ての追跡
performance.measureUserAgentSpecificMemory()
.then(result => {
console.log('Total memory:', result.bytes);
console.log('Breakdown:', result.breakdown);
});
// パフォーマンス監視の実装
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
if (entry.entryType === 'measure') {
console.log(`${entry.name}: ${entry.duration}ms`);
}
});
});
observer.observe({ entryTypes: ['measure'] });
class ObjectPool {
constructor(createFn, resetFn, maxSize = 100) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.maxSize = maxSize;
}
acquire() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return this.createFn();
}
release(obj) {
if (this.pool.length < this.maxSize) {
this.resetFn(obj);
this.pool.push(obj);
}
}
}
// 使用例:パーティクルシステム
const particlePool = new ObjectPool(
() => ({ x: 0, y: 0, velocity: { x: 0, y: 0 }, active: false }),
(particle) => {
particle.x = 0;
particle.y = 0;
particle.velocity.x = 0;
particle.velocity.y = 0;
particle.active = false;
}
);
// DOM要素に関連するメタデータの管理
const elementMetadata = new WeakMap();
function attachMetadata(element, data) {
elementMetadata.set(element, data);
}
function getMetadata(element) {
return elementMetadata.get(element);
}
// DOM要素が削除されると自動的にメタデータも削除される
class LazyImageLoader {
constructor() {
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{ rootMargin: '50px' }
);
this.loadedImages = new WeakSet();
}
observe(img) {
if (this.loadedImages.has(img)) return;
this.observer.observe(img);
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
this.loadedImages.add(img);
this.observer.unobserve(img);
}
});
}
}
// メモリ使用量の定期的な監視
class MemoryMonitor {
constructor(threshold = 100 * 1024 * 1024) { // 100MB
this.threshold = threshold;
this.interval = null;
}
start() {
this.interval = setInterval(async () => {
if (performance.memory) {
const used = performance.memory.usedJSHeapSize;
const total = performance.memory.totalJSHeapSize;
if (used > this.threshold) {
console.warn(`High memory usage: ${(used / 1024 / 1024).toFixed(2)}MB`);
// アラートを送信
this.sendAlert({
used,
total,
timestamp: new Date().toISOString()
});
}
}
}, 30000); // 30秒ごと
}
stop() {
clearInterval(this.interval);
}
sendAlert(data) {
// 監視システムへの通知
fetch('/api/alerts/memory', {
method: 'POST',
body: JSON.stringify(data)
});
}
}
メモリ最適化は重要ですが、過度な最適化は可読性を損なう可能性があります。 実際のメモリ使用状況を測定し、本当に必要な箇所にのみ最適化を適用しましょう。
// 1. メモリリークの兼典的なパターン
// イベントリスナーの解除忘れ
class ComponentWithLeak {
constructor() {
this.data = new Array(1000000).fill('data');
// リーク: アロー関数がthisを参照
window.addEventListener('resize', () => {
this.handleResize();
});
}
handleResize() {
console.log(this.data.length);
}
// 解決: 明示的なクリーンアップ
destroy() {
this.boundResize = this.handleResize.bind(this);
window.removeEventListener('resize', this.boundResize);
this.data = null;
}
}
// 2. メモリリーク検出ツール
class MemoryLeakDetector {
constructor() {
this.snapshots = [];
this.threshold = 50 * 1024 * 1024; // 50MB
}
async takeSnapshot() {
if (!performance.memory) {
console.warn('performance.memory is not available');
return;
}
const snapshot = {
timestamp: Date.now(),
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
};
this.snapshots.push(snapshot);
// メモリ使用量の増加を検出
if (this.snapshots.length > 10) {
const oldSnapshot = this.snapshots[this.snapshots.length - 10];
const increase = snapshot.usedJSHeapSize - oldSnapshot.usedJSHeapSize;
if (increase > this.threshold) {
console.error(`Memory leak detected! Increase: ${(increase / 1024 / 1024).toFixed(2)}MB`);
this.generateReport();
}
}
}
generateReport() {
const report = {
startMemory: this.snapshots[0],
currentMemory: this.snapshots[this.snapshots.length - 1],
trend: this.calculateTrend(),
recommendations: this.getRecommendations()
};
console.table(report);
return report;
}
calculateTrend() {
if (this.snapshots.length < 2) return 'insufficient data';
const deltas = [];
for (let i = 1; i < this.snapshots.length; i++) {
deltas.push(this.snapshots[i].usedJSHeapSize - this.snapshots[i-1].usedJSHeapSize);
}
const avgDelta = deltas.reduce((a, b) => a + b) / deltas.length;
if (avgDelta > 1024 * 1024) return 'increasing'; // 1MB以上の増加
if (avgDelta < -1024 * 1024) return 'decreasing';
return 'stable';
}
getRecommendations() {
const trend = this.calculateTrend();
const recommendations = [];
if (trend === 'increasing') {
recommendations.push('イベントリスナーの解除を確認');
recommendations.push('循環参照がないかチェック');
recommendations.push('大きなデータ構造の保持を見直す');
recommendations.push('WeakMap/WeakSetの使用を検討');
}
return recommendations;
}
}
// 使用例
const detector = new MemoryLeakDetector();
setInterval(() => detector.takeSnapshot(), 5000);
// 1. ガベージコレクションの最適化
// GCフレンドリーなコードパターン
class GCOptimizedClass {
constructor() {
// オブジェクトプールの活用
this.objectPool = [];
this.poolSize = 100;
// 事前割り当て
this.preallocateObjects();
}
preallocateObjects() {
for (let i = 0; i < this.poolSize; i++) {
this.objectPool.push(this.createObject());
}
}
createObject() {
return {
id: 0,
data: null,
active: false,
reset() {
this.id = 0;
this.data = null;
this.active = false;
}
};
}
getObject() {
const obj = this.objectPool.pop() || this.createObject();
obj.active = true;
return obj;
}
releaseObject(obj) {
obj.reset();
if (this.objectPool.length < this.poolSize) {
this.objectPool.push(obj);
}
}
}
// 2. メモリアロケーションの監視
class AllocationMonitor {
constructor() {
this.allocations = new Map();
this.startTime = performance.now();
}
trackAllocation(type, size) {
if (!this.allocations.has(type)) {
this.allocations.set(type, {
count: 0,
totalSize: 0,
instances: new WeakSet()
});
}
const stats = this.allocations.get(type);
stats.count++;
stats.totalSize += size;
}
getReport() {
const duration = performance.now() - this.startTime;
const report = [];
this.allocations.forEach((stats, type) => {
report.push({
type,
count: stats.count,
totalSize: stats.totalSize,
avgSize: stats.totalSize / stats.count,
allocationsPerSecond: (stats.count / duration) * 1000
});
});
return report.sort((a, b) => b.totalSize - a.totalSize);
}
}
// 3. メモリ効率的なデータ構造
class MemoryEfficientDataStructure {
constructor() {
// TypedArrayの使用(通常の配列よりメモリ効率的)
this.intBuffer = new Int32Array(10000);
this.floatBuffer = new Float32Array(10000);
// ビットフラグでメモリ節約
this.flags = new Uint8Array(10000);
}
// 複数の値を一つの数値にパック
packData(x, y, z, w) {
// 32ビットに4つの8ビット値をパック
return (x & 0xFF) | ((y & 0xFF) << 8) | ((z & 0xFF) << 16) | ((w & 0xFF) << 24);
}
unpackData(packed) {
return {
x: packed & 0xFF,
y: (packed >> 8) & 0xFF,
z: (packed >> 16) & 0xFF,
w: (packed >> 24) & 0xFF
};
}
}
// 1. Performance APIを使ったカスタムマーカー
class PerformanceProfiler {
constructor() {
this.marks = new Map();
this.measures = [];
}
startMeasure(name) {
performance.mark(`${name}-start`);
this.marks.set(name, performance.now());
}
endMeasure(name) {
performance.mark(`${name}-end`);
try {
performance.measure(
name,
`${name}-start`,
`${name}-end`
);
const duration = performance.now() - this.marks.get(name);
this.measures.push({ name, duration });
// DevToolsのパフォーマンスタブに表示
console.log(`%c${name}: ${duration.toFixed(2)}ms`, 'color: blue');
} catch (e) {
console.error(`Failed to measure ${name}:`, e);
}
}
// メモリスナップショットの自動取得
async captureHeapSnapshot() {
if ('memory' in performance && 'measureUserAgentSpecificMemory' in performance) {
try {
const measurement = await performance.measureUserAgentSpecificMemory();
console.log('Memory measurement:', measurement);
return measurement;
} catch (e) {
console.error('Memory measurement failed:', e);
}
}
}
// カスタムDevToolsプロトコル
enableCustomProtocol() {
if (typeof chrome !== 'undefined' && chrome.devtools) {
chrome.devtools.panels.create(
"Memory Profiler",
null,
"panel.html",
function(panel) {
panel.onShown.addListener(() => {
this.startProfiling();
});
}
);
}
}
}
// 2. メモリプロファイリングの自動化
class AutomatedMemoryProfiler {
constructor(options = {}) {
this.interval = options.interval || 60000; // 1分
this.maxSnapshots = options.maxSnapshots || 10;
this.snapshots = [];
}
start() {
this.intervalId = setInterval(() => {
this.takeSnapshot();
}, this.interval);
}
async takeSnapshot() {
const snapshot = {
timestamp: new Date().toISOString(),
memory: performance.memory ? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
} : null,
activeHandlers: this.countActiveHandlers(),
domNodes: document.getElementsByTagName('*').length
};
this.snapshots.push(snapshot);
if (this.snapshots.length > this.maxSnapshots) {
this.snapshots.shift();
}
this.analyzeSnapshots();
}
countActiveHandlers() {
// イベントリスナーの数を推定
let count = 0;
const elements = document.querySelectorAll('*');
elements.forEach(el => {
const listeners = getEventListeners ? getEventListeners(el) : {};
Object.keys(listeners).forEach(type => {
count += listeners[type].length;
});
});
return count;
}
analyzeSnapshots() {
if (this.snapshots.length < 2) return;
const latest = this.snapshots[this.snapshots.length - 1];
const previous = this.snapshots[this.snapshots.length - 2];
if (latest.memory && previous.memory) {
const memoryDelta = latest.memory.usedJSHeapSize - previous.memory.usedJSHeapSize;
const domDelta = latest.domNodes - previous.domNodes;
if (memoryDelta > 5 * 1024 * 1024) { // 5MB以上の増加
console.warn('メモリ使用量が急増:', {
delta: `${(memoryDelta / 1024 / 1024).toFixed(2)}MB`,
domNodes: domDelta,
timestamp: latest.timestamp
});
}
}
}
}
// 1. プロダクション向けメモリモニタリング
class ProductionMemoryMonitor {
constructor(config = {}) {
this.endpoint = config.endpoint || '/api/metrics';
this.sampleRate = config.sampleRate || 0.1; // 10%のユーザーでサンプリング
this.batchSize = config.batchSize || 10;
this.metrics = [];
}
shouldSample() {
return Math.random() < this.sampleRate;
}
collectMetrics() {
if (!this.shouldSample()) return;
const metrics = {
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
memory: this.getMemoryMetrics(),
performance: this.getPerformanceMetrics(),
errors: this.getErrorMetrics()
};
this.metrics.push(metrics);
if (this.metrics.length >= this.batchSize) {
this.sendMetrics();
}
}
getMemoryMetrics() {
if (!performance.memory) return null;
return {
used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
};
}
getPerformanceMetrics() {
const navigation = performance.getEntriesByType('navigation')[0];
return {
loadTime: navigation ? navigation.loadEventEnd - navigation.fetchStart : null,
domInteractive: navigation ? navigation.domInteractive : null,
resourceCount: performance.getEntriesByType('resource').length
};
}
getErrorMetrics() {
// エラー数の追跡(グローバルエラーハンドラと連携)
return {
errorCount: window.__errorCount || 0,
lastError: window.__lastError || null
};
}
async sendMetrics() {
if (this.metrics.length === 0) return;
const batch = this.metrics.splice(0, this.batchSize);
try {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ metrics: batch })
});
} catch (error) {
console.error('Failed to send metrics:', error);
// リトライのためにメトリクスを戻す
this.metrics.unshift(...batch);
}
}
}
// 2. メモリリークのリアルタイム通知
class MemoryLeakAlert {
constructor() {
this.baseline = null;
this.threshold = 100 * 1024 * 1024; // 100MB
this.checkInterval = 300000; // 5分
}
start() {
// ベースラインの設定
setTimeout(() => {
this.baseline = performance.memory?.usedJSHeapSize || 0;
this.scheduleCheck();
}, 10000); // 10秒後にベースラインを設定
}
scheduleCheck() {
setInterval(() => {
this.checkMemoryUsage();
}, this.checkInterval);
}
checkMemoryUsage() {
if (!performance.memory || !this.baseline) return;
const current = performance.memory.usedJSHeapSize;
const increase = current - this.baseline;
if (increase > this.threshold) {
this.sendAlert({
baseline: this.baseline,
current: current,
increase: increase,
percentage: ((increase / this.baseline) * 100).toFixed(2)
});
}
}
sendAlert(data) {
// サーバーへの通知
fetch('/api/alerts/memory-leak', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...data,
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString()
})
}).catch(console.error);
// コンソールへの警告
console.error('メモリリークの可能性:', data);
}
}
// グローバルエラーハンドラの設定
window.__errorCount = 0;
window.addEventListener('error', (event) => {
window.__errorCount++;
window.__lastError = {
message: event.message,
source: event.filename,
line: event.lineno,
column: event.colno,
timestamp: Date.now()
};
});
最適化技術 | 実装方法 | メモリ削減効果 | 適用シーン |
---|---|---|---|
オブジェクトプーリング | オブジェクトの再利用 | 70-90%削減 | ゲーム、アニメーション |
WeakMap/WeakSet | 弱い参照の使用 | 50-70%削減 | キャッシュ、メタデータ |
TypedArray | 固定サイズ配列 | 60-80%削減 | 数値データ処理 |
遅延読み込み | 必要時のみロード | 40-60%削減 | SPA、大規模アプリ |
// メモリリークが発生しやすいコード
class DataManager {
constructor() {
this.cache = {}; // 強い参照
this.handlers = [];
}
loadData(id) {
// 毎回新しいオブジェクトを作成
const data = {
id: id,
timestamp: new Date(),
values: new Array(10000).fill(0),
process: function() {
// クロージャーが大量のデータを保持
return this.values.map(v => v * 2);
}
};
this.cache[id] = data;
// イベントリスナーの蓄積
document.addEventListener('click', () => {
console.log(data.id);
});
return data;
}
clearCache() {
// 不完全なクリーンアップ
this.cache = {};
// handlersはクリアされない
}
}
// メモリ効率的なコード
class OptimizedDataManager {
constructor() {
this.cache = new WeakMap(); // 弱い参照
this.handlers = new WeakMap();
this.dataPool = []; // オブジェクトプール
}
loadData(id) {
// オブジェクトプールから取得
const data = this.dataPool.pop() || this.createDataObject();
// データの初期化
data.id = id;
data.timestamp = Date.now(); // Dateオブジェクトを避ける
// TypedArrayの使用
if (!data.values) {
data.values = new Float32Array(10000);
}
// キーオブジェクトを作成
const key = { id };
this.cache.set(key, data);
// イベントリスナーの管理
const handler = this.createHandler(data.id);
this.handlers.set(key, handler);
document.addEventListener('click', handler);
return { key, getData: () => this.cache.get(key) };
}
createDataObject() {
return {
id: null,
timestamp: null,
values: null,
// メソッドをプロトタイプに移動
};
}
createHandler(id) {
// クロージャーを最小化
return function handler(event) {
console.log(id);
};
}
releaseData(key) {
const data = this.cache.get(key);
const handler = this.handlers.get(key);
if (handler) {
document.removeEventListener('click', handler);
this.handlers.delete(key);
}
if (data) {
// オブジェクトをプールに戻す
data.id = null;
data.timestamp = null;
data.values.fill(0);
if (this.dataPool.length < 100) {
this.dataPool.push(data);
}
this.cache.delete(key);
}
}
// 完全なクリーンアップ
destroy() {
// WeakMapは自動的にクリーンアップされる
this.cache = new WeakMap();
this.handlers = new WeakMap();
this.dataPool = [];
}
}
// 使用例
const manager = new OptimizedDataManager();
const dataRef = manager.loadData('user-123');
// 必要なくなったら明示的に解放
manager.releaseData(dataRef.key);
// メモリリークが発生しやすいコード
class DataManager {
constructor() {
this.cache = {}; // 強い参照
this.handlers = [];
}
loadData(id) {
// 毎回新しいオブジェクトを作成
const data = {
id: id,
timestamp: new Date(),
values: new Array(10000).fill(0),
process: function() {
// クロージャーが大量のデータを保持
return this.values.map(v => v * 2);
}
};
this.cache[id] = data;
// イベントリスナーの蓄積
document.addEventListener('click', () => {
console.log(data.id);
});
return data;
}
clearCache() {
// 不完全なクリーンアップ
this.cache = {};
// handlersはクリアされない
}
}
// メモリ効率的なコード
class OptimizedDataManager {
constructor() {
this.cache = new WeakMap(); // 弱い参照
this.handlers = new WeakMap();
this.dataPool = []; // オブジェクトプール
}
loadData(id) {
// オブジェクトプールから取得
const data = this.dataPool.pop() || this.createDataObject();
// データの初期化
data.id = id;
data.timestamp = Date.now(); // Dateオブジェクトを避ける
// TypedArrayの使用
if (!data.values) {
data.values = new Float32Array(10000);
}
// キーオブジェクトを作成
const key = { id };
this.cache.set(key, data);
// イベントリスナーの管理
const handler = this.createHandler(data.id);
this.handlers.set(key, handler);
document.addEventListener('click', handler);
return { key, getData: () => this.cache.get(key) };
}
createDataObject() {
return {
id: null,
timestamp: null,
values: null,
// メソッドをプロトタイプに移動
};
}
createHandler(id) {
// クロージャーを最小化
return function handler(event) {
console.log(id);
};
}
releaseData(key) {
const data = this.cache.get(key);
const handler = this.handlers.get(key);
if (handler) {
document.removeEventListener('click', handler);
this.handlers.delete(key);
}
if (data) {
// オブジェクトをプールに戻す
data.id = null;
data.timestamp = null;
data.values.fill(0);
if (this.dataPool.length < 100) {
this.dataPool.push(data);
}
this.cache.delete(key);
}
}
// 完全なクリーンアップ
destroy() {
// WeakMapは自動的にクリーンアップされる
this.cache = new WeakMap();
this.handlers = new WeakMap();
this.dataPool = [];
}
}
// 使用例
const manager = new OptimizedDataManager();
const dataRef = manager.loadData('user-123');
// 必要なくなったら明示的に解放
manager.releaseData(dataRef.key);
// 大量のデータを効率的に表示
class VirtualScroller {
constructor(container, options = {}) {
this.container = container;
this.itemHeight = options.itemHeight || 50;
this.buffer = options.buffer || 5;
this.items = [];
this.visibleRange = { start: 0, end: 0 };
this.pool = new Map(); // DOM要素のプール
this.setupScrollListener();
}
setItems(items) {
this.items = items;
this.updateView();
}
setupScrollListener() {
let ticking = false;
this.container.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
this.updateView();
ticking = false;
});
ticking = true;
}
});
}
updateView() {
const scrollTop = this.container.scrollTop;
const containerHeight = this.container.clientHeight;
// 可視範囲の計算
const startIndex = Math.floor(scrollTop / this.itemHeight) - this.buffer;
const endIndex = Math.ceil((scrollTop + containerHeight) / this.itemHeight) + this.buffer;
this.visibleRange = {
start: Math.max(0, startIndex),
end: Math.min(this.items.length - 1, endIndex)
};
this.renderVisibleItems();
}
renderVisibleItems() {
const fragment = document.createDocumentFragment();
const usedElements = new Set();
// スペーサー要素(上部)
const spacerTop = document.createElement('div');
spacerTop.style.height = `${this.visibleRange.start * this.itemHeight}px`;
fragment.appendChild(spacerTop);
// 可視アイテムのレンダリング
for (let i = this.visibleRange.start; i <= this.visibleRange.end; i++) {
const item = this.items[i];
if (!item) continue;
let element = this.getPooledElement(i);
if (!element) {
element = this.createElement(item, i);
}
this.updateElement(element, item, i);
fragment.appendChild(element);
usedElements.add(element);
}
// スペーサー要素(下部)
const spacerBottom = document.createElement('div');
const remainingItems = this.items.length - this.visibleRange.end - 1;
spacerBottom.style.height = `${remainingItems * this.itemHeight}px`;
fragment.appendChild(spacerBottom);
// 未使用要素をプールに戻す
this.pool.forEach((element, index) => {
if (!usedElements.has(element)) {
this.pool.delete(index);
}
});
// DOMの更新
this.container.innerHTML = '';
this.container.appendChild(fragment);
}
getPooledElement(index) {
return this.pool.get(index);
}
createElement(item, index) {
const element = document.createElement('div');
element.className = 'virtual-item';
element.style.height = `${this.itemHeight}px`;
element.style.position = 'absolute';
element.style.top = `${index * this.itemHeight}px`;
element.style.width = '100%';
this.pool.set(index, element);
return element;
}
updateElement(element, item, index) {
element.textContent = item.text || `Item ${index}`;
element.style.top = `${index * this.itemHeight}px`;
}
}
// ワーカー間での効率的なデータ共有
class SharedMemoryProcessor {
constructor(workerCount = navigator.hardwareConcurrency || 4) {
this.workerCount = workerCount;
this.workers = [];
this.sharedBuffer = null;
this.sharedArray = null;
}
async initialize(dataSize) {
// SharedArrayBufferの作成
this.sharedBuffer = new SharedArrayBuffer(dataSize * 4); // Float32
this.sharedArray = new Float32Array(this.sharedBuffer);
// ワーカーの初期化
for (let i = 0; i < this.workerCount; i++) {
const worker = new Worker('processor-worker.js');
// SharedArrayBufferをワーカーに送信
worker.postMessage({
type: 'init',
sharedBuffer: this.sharedBuffer,
workerId: i,
workerCount: this.workerCount
});
this.workers.push(worker);
}
}
async processData(operation) {
const promises = this.workers.map((worker, index) => {
return new Promise((resolve) => {
worker.onmessage = (e) => {
if (e.data.type === 'result') {
resolve(e.data.result);
}
};
worker.postMessage({
type: 'process',
operation: operation
});
});
});
const results = await Promise.all(promises);
return this.combineResults(results);
}
combineResults(results) {
// 各ワーカーの結果を統合
return results.reduce((acc, result) => {
return acc + result;
}, 0);
}
updateData(index, value) {
// Atomicsを使った安全な更新
Atomics.store(this.sharedArray, index, value);
}
terminate() {
this.workers.forEach(worker => worker.terminate());
this.workers = [];
this.sharedBuffer = null;
this.sharedArray = null;
}
}
// processor-worker.js
let sharedArray;
let workerId;
let workerCount;
onmessage = function(e) {
const { type } = e.data;
switch (type) {
case 'init':
sharedArray = new Float32Array(e.data.sharedBuffer);
workerId = e.data.workerId;
workerCount = e.data.workerCount;
break;
case 'process':
const result = processChunk(e.data.operation);
postMessage({ type: 'result', result });
break;
}
};
function processChunk(operation) {
const chunkSize = Math.ceil(sharedArray.length / workerCount);
const start = workerId * chunkSize;
const end = Math.min(start + chunkSize, sharedArray.length);
let result = 0;
for (let i = start; i < end; i++) {
const value = Atomics.load(sharedArray, i);
switch (operation) {
case 'sum':
result += value;
break;
case 'average':
result += value / (end - start);
break;
case 'max':
result = Math.max(result, value);
break;
}
}
return result;
}
// メモリリークを自動的に検出して修復
class MemoryLeakRepairSystem {
constructor() {
this.leakPatterns = new Map();
this.repairStrategies = new Map();
this.monitoringInterval = 30000; // 30秒
this.initializePatterns();
this.startMonitoring();
}
initializePatterns() {
// リークパターンの定義
this.leakPatterns.set('event-listeners', {
detect: () => this.detectEventListenerLeaks(),
repair: () => this.repairEventListenerLeaks()
});
this.leakPatterns.set('dom-references', {
detect: () => this.detectDOMReferenceLeaks(),
repair: () => this.repairDOMReferenceLeaks()
});
this.leakPatterns.set('timers', {
detect: () => this.detectTimerLeaks(),
repair: () => this.repairTimerLeaks()
});
}
startMonitoring() {
setInterval(() => {
this.performLeakDetection();
}, this.monitoringInterval);
}
async performLeakDetection() {
const detectedLeaks = [];
for (const [name, pattern] of this.leakPatterns) {
const leakInfo = await pattern.detect();
if (leakInfo.hasLeak) {
detectedLeaks.push({
type: name,
severity: leakInfo.severity,
details: leakInfo.details
});
// 自動修復を試みる
if (leakInfo.severity === 'high') {
await pattern.repair();
}
}
}
if (detectedLeaks.length > 0) {
this.reportLeaks(detectedLeaks);
}
}
detectEventListenerLeaks() {
const elements = document.querySelectorAll('*');
let totalListeners = 0;
const suspiciousElements = [];
elements.forEach(element => {
// Chrome DevTools APIを使用(利用可能な場合)
if (typeof getEventListeners !== 'undefined') {
const listeners = getEventListeners(element);
const listenerCount = Object.values(listeners).flat().length;
totalListeners += listenerCount;
if (listenerCount > 10) {
suspiciousElements.push({
element,
count: listenerCount
});
}
}
});
return {
hasLeak: totalListeners > 1000 || suspiciousElements.length > 0,
severity: totalListeners > 5000 ? 'high' : 'medium',
details: {
totalListeners,
suspiciousElements
}
};
}
repairEventListenerLeaks() {
// グローバルなイベントリスナーマネージャーを作成
if (!window.__eventManager) {
window.__eventManager = new WeakMap();
}
// 既存のaddEventListenerをラップ
const originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
// リスナーを追跡
if (!window.__eventManager.has(this)) {
window.__eventManager.set(this, new Map());
}
const listeners = window.__eventManager.get(this);
if (!listeners.has(type)) {
listeners.set(type, new Set());
}
listeners.get(type).add(listener);
// オリジナルのメソッドを呼び出し
return originalAddEventListener.call(this, type, listener, options);
};
}
detectDOMReferenceLeaks() {
// 切り離されたDOMノードの検出
const detachedNodes = [];
const allNodes = document.querySelectorAll('*');
allNodes.forEach(node => {
if (!document.body.contains(node) && node.nodeType === 1) {
detachedNodes.push(node);
}
});
return {
hasLeak: detachedNodes.length > 100,
severity: detachedNodes.length > 500 ? 'high' : 'medium',
details: {
detachedNodesCount: detachedNodes.length
}
};
}
repairDOMReferenceLeaks() {
// DOMノードの弱い参照マップを作成
if (!window.__domReferences) {
window.__domReferences = new WeakMap();
}
// MutationObserverでDOMの変更を監視
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(node => {
// 削除されたノードのクリーンアップ
if (window.__eventManager && window.__eventManager.has(node)) {
const listeners = window.__eventManager.get(node);
listeners.forEach((listenerSet, type) => {
listenerSet.forEach(listener => {
node.removeEventListener(type, listener);
});
});
window.__eventManager.delete(node);
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
detectTimerLeaks() {
// アクティブなタイマーの数を推定
// 正確な数は取得できないため、パフォーマンスから推測
const startTime = performance.now();
let timerCount = 0;
// ダミータイマーを設定して遅延を測定
const testInterval = setInterval(() => {
timerCount++;
}, 1);
setTimeout(() => {
clearInterval(testInterval);
const elapsed = performance.now() - startTime;
const expectedCount = elapsed;
const overhead = timerCount / expectedCount;
// オーバーヘッドが大きい場合はタイマーが多い
return {
hasLeak: overhead < 0.8,
severity: overhead < 0.5 ? 'high' : 'medium',
details: {
estimatedTimers: Math.round(1 / overhead)
}
};
}, 100);
}
repairTimerLeaks() {
// タイマーの管理システムを実装
const originalSetTimeout = window.setTimeout;
const originalSetInterval = window.setInterval;
const activeTimers = new Set();
window.setTimeout = function(...args) {
const timerId = originalSetTimeout.apply(window, args);
activeTimers.add(timerId);
return timerId;
};
window.setInterval = function(...args) {
const timerId = originalSetInterval.apply(window, args);
activeTimers.add(timerId);
return timerId;
};
// クリア時に追跡から削除
const originalClearTimeout = window.clearTimeout;
const originalClearInterval = window.clearInterval;
window.clearTimeout = function(timerId) {
activeTimers.delete(timerId);
return originalClearTimeout.call(window, timerId);
};
window.clearInterval = function(timerId) {
activeTimers.delete(timerId);
return originalClearInterval.call(window, timerId);
};
// 定期的に未使用タイマーをクリア
setInterval(() => {
if (activeTimers.size > 100) {
console.warn(`Active timers: ${activeTimers.size}`);
}
}, 60000);
}
reportLeaks(leaks) {
console.group('メモリリーク検出レポート');
leaks.forEach(leak => {
console.warn(`${leak.type}: ${leak.severity}`, leak.details);
});
console.groupEnd();
// サーバーへのレポート送信
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/memory-leaks', JSON.stringify(leaks));
}
}
}
// 自動修復システムの起動
const repairSystem = new MemoryLeakRepairSystem();
A: メモリリークが疑われる場合の段階的対応方法:
パフォーマンスモニターで確認
// Chrome DevToolsでの簡易チェック
console.log('Memory:', performance.memory);
ヒープスナップショットの取得
一般的な原因のチェック
Allocation Timelineでの追跡
A: 以下の基準で使い分けます:
WeakMapを使うべき場合:
Mapを使うべき場合:
// WeakMapの例
const cache = new WeakMap();
function getComputedData(obj) {
if (cache.has(obj)) return cache.get(obj);
const data = expensiveComputation(obj);
cache.set(obj, data);
return data;
}
// Mapの例
const userSessions = new Map();
userSessions.set(userId, sessionData);
// 必要に応じて明示的に削除
userSessions.delete(userId);
A: プロダクション環境でのメモリ監視のベストプラクティス:
サンプリング戦略
メトリクスの収集
アラートの設定
分析と対応
JavaScriptのメモリ管理は、高品質な Web アプリケーション開発において不可欠な知識です。本記事で紹介した技術を活用することで、メモリ効率の良いアプリケーションを構築できます。重要なポイントは:
これらの実践により、ユーザーに快適な体験を提供できる Web アプリケーションを開発できるでしょう。