ウェブサイトの表示速度が遅い…そんな悩みを抱えている方は多いのではないでしょうか?近年のユーザーは、ページの読み込みに3秒以上かかると、実に53%の人が離脱してしまうと言われています。この問題の大きな原因となるのが、JavaScript の実行速度です。
JavaScriptは現代のウェブ開発において欠かせない技術ですが、適切な最適化を行わないと、サイトのパフォーマンスを著しく低下させてしまいます。特にランディングページやコーポレートサイトでは、表示速度の改善が直接的にコンバージョン率向上に結びつくため、JavaScript の高速化は非常に重要な課題となっています。
本記事では、JavaScript の実行速度を劇的に改善するための具体的なテクニックを、基礎から応用まで網羅的に解説します。初心者の方でも理解できるよう、実例を交えながら詳しく説明していきますので、ぜひ最後までお読みください。
目次
JavaScript高速化の基本概念
なぜJavaScriptの高速化が必要なのか?
JavaScriptの実行速度が遅いと、ユーザーエクスペリエンスが著しく低下してしまいます。特に以下のような問題が発生します:
- ページの読み込み時間の延長:重いJavaScriptファイルがページの表示をブロックし、初期表示が遅れる
- UIのレスポンスの低下:複雑な処理によってブラウザがフリーズし、ユーザーの操作に反応しなくなる
- SEOランキングの低下:Googleは表示速度をランキング要因として重視しており、遅いサイトは検索結果で不利になる
- 離脱率の増加:表示が遅いことで訪問者が離脱し、ビジネス機会を失う
これらの問題を解決するために、JavaScript の高速化技術が必要となります。
JavaScript実行モデルの理解
JavaScript の最適化を行う前に、ブラウザ内でのJavaScript実行モデルを理解することが重要です。
UIスレッドとイベントループ
JavaScript の実行は、基本的にタブ1つにつき1つのUI スレッド(メインスレッド)上で行われます。このスレッドでは、JavaScript の実行だけでなく、DOM の更新やCSS の計算、レンダリングなどの処理も行われます。
重要なのは、UIスレッドがブロックされると、すべての処理が停止してしまうことです。例えば、重い計算処理を実行している間は、ユーザーのクリックイベントも処理されません。
非同期処理とイベントループ
JavaScript では、イベントループという仕組みによって非同期処理が実現されています。この仕組みを理解することで、より効率的なコードを書くことができます。
Copy// 同期的な処理(UIスレッドをブロック)
for (let i = 0; i < 1000000; i++) {
// 重い計算処理
}
// 非同期的な処理(UIスレッドをブロックしない)
setTimeout(() => {
for (let i = 0; i < 1000000; i++) {
// 重い計算処理
}
}, 0);
JavaScript高速化の核心技術
1. ガベージコレクション(GC)を避ける最適化
ガベージコレクション(GC)は、JavaScript エンジンが不要になったメモリを自動的に回収する仕組みです。しかし、GC が実行されると JavaScript の処理が一時停止するため、パフォーマンスに悪影響を与えます。
GCを発生させる原因
Copy// 悪い例:頻繁にオブジェクトを生成(GCを誘発)
function processData() {
for (let i = 0; i < 1000; i++) {
let obj = { id: i, name: 'item' + i }; // 毎回新しいオブジェクトを生成
// 処理...
}
}
// 良い例:オブジェクトプールを使用
class ObjectPool {
constructor() {
this.pool = [];
}
getObject() {
return this.pool.length > 0 ? this.pool.pop() : { id: 0, name: '' };
}
returnObject(obj) {
obj.id = 0;
obj.name = '';
this.pool.push(obj);
}
}
GC回避の具体的手法
- オブジェクトプールの活用
- 頻繁に使用されるオブジェクトを再利用する
- 新しいオブジェクトの生成を最小限に抑える
- 配列の事前確保
Copy// 悪い例 let array = []; for (let i = 0; i < 1000; i++) { array.push(i); // 配列のサイズが動的に変わる } // 良い例 let array = new Array(1000); // 事前にサイズを確保 for (let i = 0; i < 1000; i++) { array[i] = i; }
- 文字列の最適化
Copy// 悪い例 let result = ''; for (let i = 0; i < 1000; i++) { result += 'item' + i; // 毎回新しい文字列を生成 } // 良い例 let parts = []; for (let i = 0; i < 1000; i++) { parts.push('item' + i); } let result = parts.join('');
2. メモリリークの防止
メモリリークは、使用されなくなったオブジェクトがメモリから解放されずに残る現象です。これにより、徐々にメモリ使用量が増加し、最終的にはアプリケーションのパフォーマンスが低下します。
主なメモリリークの原因と対策
- イベントリスナーの適切な削除
Copy// 悪い例 class Component { constructor() { this.handleClick = this.handleClick.bind(this); document.addEventListener('click', this.handleClick); } handleClick() { // 処理... } // destroy時にイベントリスナーを削除しない } // 良い例 class Component { constructor() { this.handleClick = this.handleClick.bind(this); document.addEventListener('click', this.handleClick); } handleClick() { // 処理... } destroy() { document.removeEventListener('click', this.handleClick); } }
- 循環参照の回避
Copy// 悪い例 function createCircularReference() { let obj1 = {}; let obj2 = {}; obj1.ref = obj2; obj2.ref = obj1; // 循環参照 return obj1; } // 良い例 function createProperReference() { let obj1 = {}; let obj2 = {}; obj1.ref = obj2; // obj2からobj1への参照は避ける return obj1; }
- DOM要素の適切な管理
Copy// 悪い例 let elements = []; function addElement() { let element = document.createElement('div'); document.body.appendChild(element); elements.push(element); // DOM要素への参照を保持 } // 良い例 let elements = []; function addElement() { let element = document.createElement('div'); document.body.appendChild(element); elements.push(element); } function removeElement(index) { if (elements[index]) { elements[index].remove(); elements[index] = null; // 参照を削除 } }
3. DOM操作の最適化
DOM操作は JavaScript の中でも特に重い処理の一つです。効率的な DOM操作を行うことで、大幅なパフォーマンス改善が期待できます。
DOM操作の最適化テクニック
- DocumentFragmentの活用
Copy// 悪い例:DOM要素を一つずつ追加 for (let i = 0; i < 1000; i++) { let div = document.createElement('div'); div.textContent = 'Item ' + i; document.body.appendChild(div); // 1000回のDOM操作 } // 良い例:DocumentFragmentを使用 let fragment = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { let div = document.createElement('div'); div.textContent = 'Item ' + i; fragment.appendChild(div); } document.body.appendChild(fragment); // 1回のDOM操作
- レイアウトスラッシングの回避
Copy// 悪い例:読み取りと書き込みを交互に実行 let elements = document.querySelectorAll('.item'); for (let i = 0; i < elements.length; i++) { elements[i].style.height = (elements[i].offsetHeight + 10) + 'px'; // offsetHeightの読み取りでレイアウトが再計算される } // 良い例:読み取りと書き込みを分離 let elements = document.querySelectorAll('.item'); let heights = []; // 最初に全ての高さを読み取り for (let i = 0; i < elements.length; i++) { heights[i] = elements[i].offsetHeight; } // 次に全ての高さを設定 for (let i = 0; i < elements.length; i++) { elements[i].style.height = (heights[i] + 10) + 'px'; }
- 仮想DOMの概念を活用
Copy// 仮想DOMライクな実装例 class VirtualDOM { constructor() { this.pendingUpdates = []; } scheduleUpdate(element, property, value) { this.pendingUpdates.push({ element, property, value }); } flushUpdates() { // 全ての更新を一度に実行 this.pendingUpdates.forEach(update => { update.element.style[update.property] = update.value; }); this.pendingUpdates = []; } }
4. ループ処理の最適化
ループ処理は JavaScript アプリケーションの中核となる部分です。効率的なループを書くことで、大幅なパフォーマンス改善が可能です。
効率的なループの書き方
- 配列の長さをキャッシュ
Copy// 悪い例 let array = new Array(1000); for (let i = 0; i < array.length; i++) { // 毎回array.lengthを評価 } // 良い例 let array = new Array(1000); let length = array.length; for (let i = 0; i < length; i++) { // lengthをキャッシュ }
- 適切なループの選択
Copylet array = [1, 2, 3, 4, 5]; // for文(最も高速) for (let i = 0; i < array.length; i++) { console.log(array[i]); } // for...of文(読みやすい) for (let item of array) { console.log(item); } // forEach(関数型的) array.forEach(item => { console.log(item); }); // map(変換処理に適している) let doubled = array.map(item => item * 2);
- 早期終了の活用
Copy// 悪い例 let found = false; for (let i = 0; i < array.length; i++) { if (array[i] === target) { found = true; } } // 良い例 let found = false; for (let i = 0; i < array.length; i++) { if (array[i] === target) { found = true; break; // 早期終了 } }
5. 非同期処理の最適化
非同期処理を適切に活用することで、UIスレッドをブロックすることなく、スムーズなユーザーエクスペリエンスを提供できます。
非同期処理の効果的な活用法
- Promise と async/await の活用
Copy// 悪い例:同期的な処理 function fetchData() { let result1 = syncApiCall1(); let result2 = syncApiCall2(); let result3 = syncApiCall3(); return processData(result1, result2, result3); } // 良い例:並行処理 async function fetchData() { let [result1, result2, result3] = await Promise.all([ asyncApiCall1(), asyncApiCall2(), asyncApiCall3() ]); return processData(result1, result2, result3); }
- Web Worker の活用
Copy// メインスレッド function processLargeData(data) { return new Promise((resolve) => { let worker = new Worker('worker.js'); worker.postMessage(data); worker.onmessage = (e) => { resolve(e.data); worker.terminate(); }; }); } // worker.js self.onmessage = function(e) { let data = e.data; // 重い処理を実行 let result = heavyComputation(data); self.postMessage(result); };
- RequestAnimationFrame の活用
Copy// 悪い例:setTimeoutでアニメーション function animate() { updateAnimation(); setTimeout(animate, 16); // 約60fps } // 良い例:requestAnimationFrameでアニメーション function animate() { updateAnimation(); requestAnimationFrame(animate); }
JavaScript読み込みの最適化
読み込み方式の選択
JavaScript ファイルの読み込み方式は、サイトの表示速度に大きく影響します。それぞれの特徴を理解して、適切な方式を選択することが重要です。
各読み込み方式の特徴
- defer属性の活用
Copy<!-- 基本的にはこれを使用 --> <script defer src="./script.js"></script>
defer属性を使用すると、スクリプトはバックグラウンドで読み込まれ、HTMLの解析が完了してから実行されます。これにより、ページの表示をブロックすることなく、スクリプトを読み込むことができます。 - async属性の特別な使用例
Copy<!-- 広告やアナリティクスなどに使用 --> <script async src="https://www.google-analytics.com/analytics.js"></script>
async属性は、スクリプトの読み込みが完了次第すぐに実行されるため、Google Analytics やGoogle AdSense のようなサードパーティスクリプトに適しています。 - インライン配置の戦略的活用
Copy<!-- ファーストビューに必要な最小限のスクリプト --> <script> // クリティカルなJavaScriptのみをインライン配置 document.documentElement.className = 'js-enabled'; </script>
読み込み位置の最適化
JavaScript の読み込み位置は、非同期読み込みを前提とすると、<head>
タグ内に配置することが推奨されます。これにより、HTMLの解析と並行してスクリプトの読み込みが開始され、全体的な読み込み時間が短縮されます。
Copy<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>最適化されたページ</title>
<!-- 非同期読み込みなので、headに配置 -->
<script defer src="./main.js"></script>
<script async src="./analytics.js"></script>
</head>
<body>
<!-- コンテンツ -->
</body>
</html>
JavaScript高速化テクニック
1. JavaScript エンジンの最適化メカニズム
現代のJavaScript エンジンは、非常に高度な最適化を自動的に実行しています。これらの最適化メカニズムを理解することで、より効率的なコードを書くことができます。
Inline Caching(インラインキャッシング)
JavaScript エンジンは、オブジェクトのプロパティアクセスパターンを学習し、キャッシュを構築します。
Copy// 良い例:一貫したオブジェクト構造
function createPerson(name, age) {
return {
name: name,
age: age,
greet: function() {
return `Hello, I'm ${this.name}`;
}
};
}
// 悪い例:一貫性のないオブジェクト構造
function createPerson(name, age, address) {
let person = { name: name, age: age };
if (address) {
person.address = address; // 条件的にプロパティを追加
}
return person;
}
Hidden Class(隠れクラス)
JavaScript エンジンは、同じ形状のオブジェクトに対して隠れクラスを作成し、プロパティアクセスを最適化します。
Copy// 良い例:同じ順序でプロパティを設定
function Point(x, y) {
this.x = x;
this.y = y;
}
// 悪い例:異なる順序でプロパティを設定
function Point1(x, y) {
this.x = x;
this.y = y;
}
function Point2(x, y) {
this.y = y; // 異なる順序
this.x = x;
}
2. メモリ使用量の最適化
効率的なメモリ使用は、パフォーマンスの向上に直結します。
WeakMap と WeakSet の活用
Copy// 通常のMapはメモリリークの原因になる可能性
let map = new Map();
let obj = {};
map.set(obj, 'some value');
obj = null; // objを削除してもMapから参照されているため、GCされない
// WeakMapを使用してメモリリークを防ぐ
let weakMap = new WeakMap();
let obj2 = {};
weakMap.set(obj2, 'some value');
obj2 = null; // objが削除されると、WeakMapからも自動的に削除される
効率的なデータ構造の選択
Copy// 配列 vs Set vs Map のパフォーマンス比較
let array = [1, 2, 3, 4, 5];
let set = new Set([1, 2, 3, 4, 5]);
let map = new Map([[1, true], [2, true], [3, true], [4, true], [5, true]]);
// 存在確認のパフォーマンス
console.time('array');
array.includes(3); // O(n)
console.timeEnd('array');
console.time('set');
set.has(3); // O(1)
console.timeEnd('set');
console.time('map');
map.has(3); // O(1)
console.timeEnd('map');
3. 関数の最適化
関数の書き方や呼び出し方を最適化することで、実行速度を向上させることができます。
関数の最適化テクニック
Copy// 1. 関数の純粋性を保つ
function pureFunction(a, b) {
return a + b; // 副作用なし、同じ入力に対して同じ出力
}
// 2. メモ化(memoization)の活用
function memoize(fn) {
let cache = {};
return function(...args) {
let key = JSON.stringify(args);
if (key in cache) {
return cache[key];
}
let result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
let fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
// 3. 関数の事前バインド
class EventHandler {
constructor() {
this.handleClick = this.handleClick.bind(this); // 事前バインド
}
handleClick() {
// イベントハンドラーの処理
}
}
実践的な最適化事例
ケーススタディ1: ECサイトの商品一覧ページ
ECサイトの商品一覧ページでは、大量の商品データを効率的に表示する必要があります。以下は、実際の最適化事例です。
問題:大量の商品データによるパフォーマンス低下
Copy// 最適化前:全商品を一度に表示
function renderProducts(products) {
let container = document.getElementById('product-list');
products.forEach(product => {
let productElement = createProductElement(product);
container.appendChild(productElement); // 大量のDOM操作
});
}
解決策:仮想スクロールとlazy loading
Copy// 最適化後:仮想スクロール実装
class VirtualScroll {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleItems = 10;
this.startIndex = 0;
this.setupContainer();
this.render();
this.attachScrollListener();
}
setupContainer() {
this.container.style.height = this.items.length * this.itemHeight + 'px';
this.container.style.overflow = 'auto';
}
render() {
let endIndex = Math.min(this.startIndex + this.visibleItems, this.items.length);
let fragment = document.createDocumentFragment();
for (let i = this.startIndex; i < endIndex; i++) {
let item = this.createItem(this.items[i], i);
fragment.appendChild(item);
}
this.container.innerHTML = '';
this.container.appendChild(fragment);
}
createItem(itemData, index) {
let item = document.createElement('div');
item.style.position = 'absolute';
item.style.top = index * this.itemHeight + 'px';
item.style.height = this.itemHeight + 'px';
item.textContent = itemData.name;
return item;
}
attachScrollListener() {
let lastScrollTop = 0;
this.container.addEventListener('scroll', () => {
let scrollTop = this.container.scrollTop;
let newStartIndex = Math.floor(scrollTop / this.itemHeight);
if (newStartIndex !== this.startIndex) {
this.startIndex = newStartIndex;
this.render();
}
});
}
}
ケーススタディ2: ランディングページのアニメーション最適化
ランディングページでは、魅力的なアニメーションが重要ですが、パフォーマンスとのバランスが求められます。
問題:アニメーションによるカクつき
Copy// 最適化前:setTimeoutを使用したアニメーション
function animateElement(element, from, to, duration) {
let start = Date.now();
let timer = setInterval(() => {
let elapsed = Date.now() - start;
let progress = elapsed / duration;
if (progress >= 1) {
element.style.transform = `translateX(${to}px)`;
clearInterval(timer);
} else {
let current = from + (to - from) * progress;
element.style.transform = `translateX(${current}px)`;
}
}, 16); // 約60fps
}
解決策:requestAnimationFrameとCSSトランジション
Copy// 最適化後:requestAnimationFrameを使用
function animateElement(element, from, to, duration) {
let start = null;
function animate(timestamp) {
if (!start) start = timestamp;
let elapsed = timestamp - start;
let progress = Math.min(elapsed / duration, 1);
// イージング関数を適用
let easedProgress = easeInOutCubic(progress);
let current = from + (to - from) * easedProgress;
element.style.transform = `translateX(${current}px)`;
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
}
// さらに最適化:CSS transitionを活用
function animateElementWithCSS(element, to, duration) {
element.style.transition = `transform ${duration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
element.style.transform = `translateX(${to}px)`;
}
パフォーマンス測定と分析
1. 測定ツールの活用
JavaScript のパフォーマンスを改善するには、まず現状を正確に把握することが重要です。
Chrome DevTools Performance パネル
Copy// パフォーマンス測定のコード例
function measurePerformance(func, name) {
console.time(name);
performance.mark(`${name}-start`);
let result = func();
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
console.timeEnd(name);
return result;
}
// 使用例
let result = measurePerformance(() => {
return expensiveOperation();
}, 'expensive-operation');
User Timing API の活用
Copy// 詳細なパフォーマンス測定
class PerformanceMonitor {
static startMeasure(name) {
performance.mark(`${name}-start`);
}
static endMeasure(name) {
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
}
static getResults() {
return performance.getEntriesByType('measure');
}
static clearAll() {
performance.clearMarks();
performance.clearMeasures();
}
}
// 使用例
PerformanceMonitor.startMeasure('dom-update');
updateDOMElements();
PerformanceMonitor.endMeasure('dom-update');
console.log(PerformanceMonitor.getResults());
2. メモリ使用量の監視
// メモリ使用量の監視
function monitorMemoryUsage() {
if (performance.memory) {
console.log({
used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + ' MB',
total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + ' MB',
limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024) + ' MB'
});
}
}
// 定期的なメモリ監視
setInterval(monitorMemoryUsage, 5000);
// メモリリークの検出
class MemoryLeakDetector {
constructor() {
this.samples = [];
this.interval = null;
}
start() {
this.interval = setInterval(() => {
if (performance.memory) {
this.samples.push({
timestamp: Date.now(),
usedMemory: performance.memory.usedJSHeapSize
});
// 古いサンプルを削除(直近5分間のみ保持)
let fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
this.samples = this.samples.filter(sample =>
sample.timestamp > fiveMinutesAgo
);
this.checkForLeaks();
}
}, 10000); // 10秒間隔
}
stop() {
if (this.interval) {
clearInterval(this.interval);
}
}
checkForLeaks() {
if (this.samples.length < 10) return;
// メモリ使用量の傾向を分析
let recent = this.samples.slice(-5);
let older = this.samples.slice(-10, -5);
let recentAvg = recent.reduce((sum, s) => sum + s.usedMemory, 0) / recent.length;
let olderAvg = older.reduce((sum, s) => sum + s.usedMemory, 0) / older.length;
// 10%以上の増加がある場合は警告
if (recentAvg > olderAvg * 1.1) {
console.warn('メモリリークの可能性があります:', {
increase: Math.round((recentAvg - olderAvg) / 1024 / 1024) + ' MB'
});
}
}
}und(performance.memory.totalJSHeapSize / 1024 / 1024) + ' MB
});
}
}
}
最新のJavaScript最適化技術
1. ES6+の最適化機能
現代のJavaScriptエンジンは、ES6以降の機能を高度に最適化しています。これらの機能を活用することで、より効率的なコードを書くことができます。
分割代入とスプレッド演算子の効率的な使用
Copy// 効率的な分割代入
function processUserData({name, age, email, ...rest}) {
// 必要なプロパティのみを取得
return {
displayName: name,
userAge: age,
contactEmail: email,
metadata: rest
};
}
// 配列の効率的な操作
function mergeArrays(arr1, arr2, arr3) {
// スプレッド演算子を使用した効率的な結合
return [...arr1, ...arr2, ...arr3];
}
// Map/Set の活用
class UniqueValueProcessor {
constructor() {
this.uniqueValues = new Set();
this.valueMap = new Map();
}
addValue(key, value) {
this.uniqueValues.add(value);
this.valueMap.set(key, value);
}
hasValue(value) {
return this.uniqueValues.has(value); // O(1)の検索
}
}
テンプレートリテラルの最適化
Copy// 効率的なテンプレートリテラル使用
function createHTML(items) {
// 大量のデータの場合は配列を使用
let parts = [];
for (let item of items) {
parts.push(`<div class="item">${item.name}</div>`);
}
return parts.join('');
}
// タグ付きテンプレートリテラルの活用
function html(strings, ...values) {
let result = '';
for (let i = 0; i < strings.length; i++) {
result += strings[i];
if (i < values.length) {
// XSS対策を含む値のエスケープ
result += String(values[i]).replace(/[&<>"']/g, match => {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return escapeMap[match];
});
}
}
return result;
}
2. Web Workers の活用
重い計算処理をメインスレッドから分離することで、UIの応答性を保つことができます。
Copy// メインスレッド側
class WorkerPool {
constructor(workerScript, poolSize = 4) {
this.workers = [];
this.availableWorkers = [];
this.taskQueue = [];
for (let i = 0; i < poolSize; i++) {
let worker = new Worker(workerScript);
this.workers.push(worker);
this.availableWorkers.push(worker);
}
}
execute(data) {
return new Promise((resolve, reject) => {
let task = { data, resolve, reject };
if (this.availableWorkers.length > 0) {
this.runTask(task);
} else {
this.taskQueue.push(task);
}
});
}
runTask(task) {
let worker = this.availableWorkers.pop();
worker.onmessage = (e) => {
task.resolve(e.data);
this.availableWorkers.push(worker);
// キューに待機中のタスクがあれば実行
if (this.taskQueue.length > 0) {
this.runTask(this.taskQueue.shift());
}
};
worker.onerror = (e) => {
task.reject(e);
this.availableWorkers.push(worker);
};
worker.postMessage(task.data);
}
terminate() {
this.workers.forEach(worker => worker.terminate());
}
}
// 使用例
let workerPool = new WorkerPool('calculation-worker.js');
async function processLargeDataset(dataset) {
let chunks = chunkArray(dataset, 1000);
let promises = chunks.map(chunk => workerPool.execute(chunk));
let results = await Promise.all(promises);
return results.flat();
}
3. Service Worker による最適化
Service Workerを活用することで、リソースのキャッシュやバックグラウンド同期を実現できます。
Copy// service-worker.js
const CACHE_NAME = 'app-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/scripts/vendor.js'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// キャッシュにある場合はそれを返す
if (response) {
return response;
}
// なければネットワークから取得
return fetch(event.request).then((response) => {
// レスポンスが有効でない場合はそのまま返す
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// レスポンスをキャッシュに保存
let responseToCache = response.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// メインスレッド側での Service Worker 登録
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then((registration) => {
console.log('Service Worker registered:', registration);
})
.catch((error) => {
console.log('Service Worker registration failed:', error);
});
}
ランディングページに特化した最適化
LandingHub を活用した表示速度改善
ランディングページの表示速度は、コンバージョン率に直結する重要な要素です。LandingHubのようなランディングページ作成ツールを使用する際も、JavaScript の最適化は重要です。
ランディングページ特有の最適化ポイント
Copy// 1. ファーストビューの最適化
class FirstViewOptimizer {
constructor() {
this.criticalElements = document.querySelectorAll('[data-critical]');
this.deferredElements = document.querySelectorAll('[data-deferred]');
}
init() {
// クリティカルな要素を最初に処理
this.processCriticalElements();
// 非クリティカルな要素は遅延読み込み
this.setupDeferredLoading();
}
processCriticalElements() {
this.criticalElements.forEach(element => {
// ファーストビューに必要な処理のみ実行
this.applyImmediateEffects(element);
});
}
setupDeferredLoading() {
// Intersection Observer を使用した遅延読み込み
let observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadDeferredContent(entry.target);
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px' // 50px手前で読み込み開始
});
this.deferredElements.forEach(element => {
observer.observe(element);
});
}
loadDeferredContent(element) {
// 遅延読み込みの実装
let src = element.dataset.src;
if (src) {
element.src = src;
}
// アニメーションの適用
element.classList.add('loaded');
}
}
// 2. フォームの最適化
class FormOptimizer {
constructor(form) {
this.form = form;
this.validationRules = {};
this.debounceTimers = {};
}
init() {
this.setupRealTimeValidation();
this.setupFormSubmission();
}
setupRealTimeValidation() {
let inputs = this.form.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
input.addEventListener('input', (e) => {
this.debounceValidation(e.target);
});
});
}
debounceValidation(input) {
let fieldName = input.name;
// 既存のタイマーをクリア
if (this.debounceTimers[fieldName]) {
clearTimeout(this.debounceTimers[fieldName]);
}
// 新しいタイマーを設定
this.debounceTimers[fieldName] = setTimeout(() => {
this.validateField(input);
}, 300);
}
validateField(input) {
let isValid = this.performValidation(input);
this.updateFieldUI(input, isValid);
}
performValidation(input) {
// バリデーションロジック
let value = input.value.trim();
let rules = this.validationRules[input.name];
if (!rules) return true;
for (let rule of rules) {
if (!rule.test(value)) {
return false;
}
}
return true;
}
updateFieldUI(input, isValid) {
let container = input.closest('.form-field');
if (!container) return;
if (isValid) {
container.classList.remove('error');
container.classList.add('valid');
} else {
container.classList.remove('valid');
container.classList.add('error');
}
}
}
A/Bテスト用の軽量な実装
Copy// A/Bテストの最適化実装
class LightweightABTester {
constructor() {
this.variant = this.getVariant();
this.events = [];
}
getVariant() {
// 既存のvariantがあればそれを使用
let stored = localStorage.getItem('ab-variant');
if (stored) return stored;
// 新しいvariantを生成
let variant = Math.random() < 0.5 ? 'A' : 'B';
localStorage.setItem('ab-variant', variant);
return variant;
}
applyVariant() {
document.body.classList.add(`variant-${this.variant}`);
// variant固有の処理
if (this.variant === 'B') {
this.applyVariantB();
}
}
applyVariantB() {
// バリアントBの変更を適用
let ctaButton = document.querySelector('.cta-button');
if (ctaButton) {
ctaButton.textContent = 'お得に始める';
ctaButton.style.backgroundColor = '#ff6b6b';
}
}
trackEvent(eventName, data = {}) {
this.events.push({
event: eventName,
variant: this.variant,
timestamp: Date.now(),
data: data
});
// イベントを外部に送信(バッチ処理)
this.sendEventsIfReady();
}
sendEventsIfReady() {
if (this.events.length >= 10) {
this.sendEvents();
}
}
sendEvents() {
if (this.events.length === 0) return;
// navigator.sendBeacon を使用した効率的な送信
let payload = JSON.stringify(this.events);
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics', payload);
} else {
// フォールバック
fetch('/api/analytics', {
method: 'POST',
body: payload,
headers: {
'Content-Type': 'application/json'
}
});
}
this.events = [];
}
}
コンバージョン改善のための最適化
Copy// コンバージョン率向上のための最適化
class ConversionOptimizer {
constructor() {
this.exitIntentDetected = false;
this.scrollDepth = 0;
this.timeOnPage = 0;
this.startTime = Date.now();
}
init() {
this.setupExitIntentDetection();
this.setupScrollTracking();
this.setupTimeTracking();
this.setupFormAnalytics();
}
setupExitIntentDetection() {
document.addEventListener('mouseleave', (e) => {
if (e.clientY <= 0 && !this.exitIntentDetected) {
this.exitIntentDetected = true;
this.showExitIntentPopup();
}
});
}
setupScrollTracking() {
let ticking = false;
document.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
this.updateScrollDepth();
ticking = false;
});
ticking = true;
}
});
}
updateScrollDepth() {
let windowHeight = window.innerHeight;
let documentHeight = document.documentElement.scrollHeight;
let scrollTop = window.pageYOffset;
let currentDepth = Math.round(((scrollTop + windowHeight) / documentHeight) * 100);
if (currentDepth > this.scrollDepth) {
this.scrollDepth = currentDepth;
// 特定のスクロール深度でイベントを発火
if (this.scrollDepth >= 50 && !this.midScrollReached) {
this.midScrollReached = true;
this.trackEvent('scroll_50_percent');
}
}
}
setupTimeTracking() {
setInterval(() => {
this.timeOnPage = Date.now() - this.startTime;
// 30秒滞在でイベント発火
if (this.timeOnPage >= 30000 && !this.thirtySecondReached) {
this.thirtySecondReached = true;
this.trackEvent('time_30_seconds');
}
}, 1000);
}
setupFormAnalytics() {
let forms = document.querySelectorAll('form');
forms.forEach(form => {
let fields = form.querySelectorAll('input, select, textarea');
fields.forEach(field => {
field.addEventListener('focus', () => {
this.trackEvent('form_field_focus', { field: field.name });
});
field.addEventListener('blur', () => {
if (field.value.trim()) {
this.trackEvent('form_field_completed', { field: field.name });
}
});
});
form.addEventListener('submit', (e) => {
this.trackEvent('form_submit_attempt');
});
});
}
showExitIntentPopup() {
// 軽量なポップアップ表示
let popup = document.createElement('div');
popup.className = 'exit-intent-popup';
popup.innerHTML = `
<div class="popup-content">
<h3>お待ちください!</h3>
<p>今なら特別価格でご提供中です</p>
<button class="popup-cta">詳細を見る</button>
<button class="popup-close">×</button>
</div>
`;
document.body.appendChild(popup);
// イベントリスナーの設定
popup.querySelector('.popup-close').addEventListener('click', () => {
popup.remove();
});
popup.querySelector('.popup-cta').addEventListener('click', () => {
this.trackEvent('exit_intent_popup_click');
popup.remove();
});
this.trackEvent('exit_intent_popup_shown');
}
trackEvent(eventName, data = {}) {
// 分析用のイベント追跡
console.log('Event tracked:', eventName, data);
}
}
最適化の効果測定
パフォーマンス改善の定量的評価
Copy// パフォーマンステストスイート
class PerformanceTestSuite {
constructor() {
this.results = [];
this.baseline = null;
}
async runTests() {
console.log('パフォーマンステストを開始します...');
// ベースラインの取得
this.baseline = await this.measureBaseline();
// 各種テストの実行
await this.testDOMManipulation();
await this.testEventHandling();
await this.testMemoryUsage();
await this.testNetworkRequests();
this.generateReport();
}
async measureBaseline() {
let start = performance.now();
// 基本的な処理の実行
for (let i = 0; i < 10000; i++) {
let div = document.createElement('div');
div.textContent = `Item ${i}`;
}
let end = performance.now();
return end - start;
}
async testDOMManipulation() {
let tests = [
{
name: 'innerHTML vs createElement',
test: () => this.compareInnerHTMLvsCreateElement()
},
{
name: 'DocumentFragment vs Direct Append',
test: () => this.compareFragmentVsDirect()
},
{
name: 'QuerySelector vs GetElementById',
test: () => this.compareSelectors()
}
];
for (let test of tests) {
let result = await test.test();
this.results.push({
category: 'DOM Manipulation',
name: test.name,
result: result
});
}
}
compareInnerHTMLvsCreateElement() {
let container1 = document.createElement('div');
let container2 = document.createElement('div');
// innerHTML test
let start1 = performance.now();
let html = '';
for (let i = 0; i < 1000; i++) {
html += `<div>Item ${i}</div>`;
}
container1.innerHTML = html;
let end1 = performance.now();
// createElement test
let start2 = performance.now();
let fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
let div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
container2.appendChild(fragment);
let end2 = performance.now();
return {
innerHTML: end1 - start1,
createElement: end2 - start2,
winner: (end1 - start1) < (end2 - start2) ? 'innerHTML' : 'createElement'
};
}
compareFragmentVsDirect() {
let container1 = document.createElement('div');
let container2 = document.createElement('div');
document.body.appendChild(container1);
document.body.appendChild(container2);
// Direct append test
let start1 = performance.now();
for (let i = 0; i < 1000; i++) {
let div = document.createElement('div');
div.textContent = `Item ${i}`;
container1.appendChild(div);
}
let end1 = performance.now();
// Fragment test
let start2 = performance.now();
let fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
let div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
container2.appendChild(fragment);
let end2 = performance.now();
// クリーンアップ
container1.remove();
container2.remove();
return {
direct: end1 - start1,
fragment: end2 - start2,
winner: (end1 - start1) < (end2 - start2) ? 'direct' : 'fragment'
};
}
compareSelectors() {
// テスト用要素の準備
let testContainer = document.createElement('div');
testContainer.id = 'test-container';
for (let i = 0; i < 1000; i++) {
let div = document.createElement('div');
div.className = 'test-item';
div.id = `item-${i}`;
testContainer.appendChild(div);
}
document.body.appendChild(testContainer);
// getElementById test
let start1 = performance.now();
for (let i = 0; i < 1000; i++) {
document.getElementById(`item-${i}`);
}
let end1 = performance.now();
// querySelector test
let start2 = performance.now();
for (let i = 0; i < 1000; i++) {
document.querySelector(`#item-${i}`);
}
let end2 = performance.now();
// クリーンアップ
testContainer.remove();
return {
getElementById: end1 - start1,
querySelector: end2 - start2,
winner: (end1 - start1) < (end2 - start2) ? 'getElementById' : 'querySelector'
};
}
async testMemoryUsage() {
if (!performance.memory) {
this.results.push({
category: 'Memory',
name: 'Memory API',
result: 'Not supported'
});
return;
}
let initialMemory = performance.memory.usedJSHeapSize;
// メモリを大量に使用する処理
let bigArray = [];
for (let i = 0; i < 100000; i++) {
bigArray.push({
id: i,
data: 'x'.repeat(100)
});
}
let peakMemory = performance.memory.usedJSHeapSize;
// メモリの解放
bigArray = null;
// 強制的なガベージコレクション(開発時のみ)
if (window.gc) {
window.gc();
}
// 少し待ってからメモリ使用量を確認
await new Promise(resolve => setTimeout(resolve, 1000));
let finalMemory = performance.memory.usedJSHeapSize;
this.results.push({
category: 'Memory',
name: 'Memory Usage Test',
result: {
initial: Math.round(initialMemory / 1024 / 1024) + ' MB',
peak: Math.round(peakMemory / 1024 / 1024) + ' MB',
final: Math.round(finalMemory / 1024 / 1024) + ' MB',
increase: Math.round((peakMemory - initialMemory) / 1024 / 1024) + ' MB',
leaked: Math.round((finalMemory - initialMemory) / 1024 / 1024) + ' MB'
}
});
}
async testNetworkRequests() {
let testUrls = [
'https://jsonplaceholder.typicode.com/posts/1',
'https://jsonplaceholder.typicode.com/posts/2',
'https://jsonplaceholder.typicode.com/posts/3'
];
// 順次実行
let start1 = performance.now();
for (let url of testUrls) {
try {
await fetch(url);
} catch (e) {
// エラーは無視
}
}
let end1 = performance.now();
// 並行実行
let start2 = performance.now();
try {
await Promise.all(testUrls.map(url => fetch(url)));
} catch (e) {
// エラーは無視
}
let end2 = performance.now();
this.results.push({
category: 'Network',
name: 'Sequential vs Parallel Requests',
result: {
sequential: Math.round(end1 - start1) + ' ms',
parallel: Math.round(end2 - start2) + ' ms',
improvement: Math.round(((end1 - start1) - (end2 - start2)) / (end1 - start1) * 100) + '%'
}
});
}
generateReport() {
console.log('\n=== パフォーマンステスト結果 ===');
console.log(`ベースライン: ${this.baseline.toFixed(2)} ms\n`);
let categories = {};
this.results.forEach(result => {
if (!categories[result.category]) {
categories[result.category] = [];
}
categories[result.category].push(result);
});
Object.keys(categories).forEach(category => {
console.log(`[${category}]`);
categories[category].forEach(test => {
console.log(` ${test.name}:`, test.result);
});
console.log('');
});
}
}
// 使用例
// let testSuite = new PerformanceTestSuite();
// testSuite.runTests();
実装のベストプラクティス
1. 段階的な最適化アプローチ
JavaScript の最適化は一気に行うのではなく、段階的に進めることが重要です。
Copy// 最適化の優先順位付け
class OptimizationManager {
constructor() {
this.optimizations = [
{
name: 'DOM操作の最適化',
priority: 1,
impact: 'high',
effort: 'medium',
implement: () => this.optimizeDOM()
},
{
name: 'イベントリスナーの最適化',
priority: 2,
impact: 'medium',
effort: 'low',
implement: () => this.optimizeEventListeners()
},
{
name: 'メモリ管理の改善',
priority: 3,
impact: 'high',
effort: 'high',
implement: () => this.optimizeMemory()
},
{
name: '非同期処理の最適化',
priority: 4,
impact: 'medium',
effort: 'medium',
implement: () => this.optimizeAsync()
}
];
}
implementOptimizations() {
// 優先順位順にソート
this.optimizations.sort((a, b) => a.priority - b.priority);
console.log('最適化の実装を開始します...');
this.optimizations.forEach((optimization, index) => {
console.log(`${index + 1}. ${optimization.name} (影響度: ${optimization.impact}, 工数: ${optimization.effort})`);
try {
optimization.implement();
console.log(`✓ ${optimization.name} が完了しました`);
} catch (error) {
console.error(`✗ ${optimization.name} の実装中にエラーが発生しました:`, error);
}
});
}
optimizeDOM() {
// DOM操作の最適化実装
this.implementDocumentFragment();
this.implementVirtualScrolling();
this.implementLazyLoading();
}
optimizeEventListeners() {
// イベントリスナーの最適化実装
this.implementEventDelegation();
this.implementPassiveListeners();
}
optimizeMemory() {
// メモリ管理の最適化実装
this.implementObjectPooling();
this.implementWeakReferences();
}
optimizeAsync() {
// 非同期処理の最適化実装
this.implementPromiseOptimization();
this.implementWebWorkers();
}
}
2. 継続的なパフォーマンス監視
Copy// パフォーマンス監視システム
class PerformanceMonitor {
constructor() {
this.metrics = {
pageLoad: [],
jsExecution: [],
domInteraction: [],
memoryUsage: []
};
this.thresholds = {
pageLoad: 3000, // 3秒
jsExecution: 100, // 100ms
domInteraction: 16, // 16ms (60fps)
memoryUsage: 100 // 100MB
};
this.isMonitoring = false;
}
startMonitoring() {
if (this.isMonitoring) return;
this.isMonitoring = true;
this.monitorPageLoad();
this.monitorJSExecution();
this.monitorDOMInteraction();
this.monitorMemoryUsage();
// 定期的なレポート生成
this.reportInterval = setInterval(() => {
this.generateReport();
}, 60000); // 1分間隔
}
stopMonitoring() {
this.isMonitoring = false;
if (this.reportInterval) {
clearInterval(this.reportInterval);
}
}
monitorPageLoad() {
// Navigation Timing API を使用
window.addEventListener('load', () => {
let timing = performance.getEntriesByType('navigation')[0];
let loadTime = timing.loadEventEnd - timing.navigationStart;
this.recordMetric('pageLoad', loadTime);
if (loadTime > this.thresholds.pageLoad) {
this.alertSlowPerformance('pageLoad', loadTime);
}
});
}
monitorJSExecution() {
// Long Task API を使用(対応ブラウザのみ)
if ('PerformanceObserver' in window) {
let observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'longtask') {
this.recordMetric('jsExecution', entry.duration);
if (entry.duration > this.thresholds.jsExecution) {
this.alertSlowPerformance('jsExecution', entry.duration);
}
}
});
});
observer.observe({ entryTypes: ['longtask'] });
}
}
monitorDOMInteraction() {
// First Input Delay を監視
if ('PerformanceObserver' in window) {
let observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'first-input') {
let delay = entry.processingStart - entry.startTime;
this.recordMetric('domInteraction', delay);
if (delay > this.thresholds.domInteraction) {
this.alertSlowPerformance('domInteraction', delay);
}
}
});
});
observer.observe({ entryTypes: ['first-input'] });
}
}
monitorMemoryUsage() {
if (performance.memory) {
setInterval(() => {
let used = performance.memory.usedJSHeapSize / 1024 / 1024; // MB
this.recordMetric('memoryUsage', used);
if (used > this.thresholds.memoryUsage) {
this.alertSlowPerformance('memoryUsage', used);
}
}, 5000); // 5秒間隔
}
}
recordMetric(type, value) {
this.metrics[type].push({
timestamp: Date.now(),
value: value
});
// 古いデータを削除(直近1時間のみ保持)
let oneHourAgo = Date.now() - 60 * 60 * 1000;
this.metrics[type] = this.metrics[type].filter(
metric => metric.timestamp > oneHourAgo
);
}
alertSlowPerformance(type, value) {
console.warn(`パフォーマンス警告: ${type} = ${value}ms (閾値: ${this.thresholds[type]}ms)`);
// 実際のアプリケーションでは、ここで外部監視システムに通知
this.sendAlertToMonitoringSystem(type, value);
}
sendAlertToMonitoringSystem(type, value) {
// 監視システムへの通知実装
if (navigator.sendBeacon) {
let data = JSON.stringify({
type: 'performance_alert',
metric: type,
value: value,
threshold: this.thresholds[type],
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
});
navigator.sendBeacon('/api/performance-alerts', data);
}
}
generateReport() {
let report = {
timestamp: new Date().toISOString(),
metrics: {}
};
Object.keys(this.metrics).forEach(type => {
let values = this.metrics[type].map(m => m.value);
if (values.length > 0) {
report.metrics[type] = {
count: values.length,
average: values.reduce((a, b) => a + b, 0) / values.length,
min: Math.min(...values),
max: Math.max(...values),
p95: this.calculatePercentile(values, 95)
};
}
});
console.log('パフォーマンスレポート:', report);
return report;
}
calculatePercentile(values, percentile) {
let sorted = values.sort((a, b) => a - b);
let index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[index];
}
}
3. エラーハンドリングとフォールバック
Copy// 堅牢なエラーハンドリング
class RobustJavaScriptExecutor {
constructor() {
this.errorCount = 0;
this.maxErrors = 10;
this.fallbackMode = false;
}
executeWithFallback(primaryFunction, fallbackFunction, context = null) {
try {
return primaryFunction.call(context);
} catch (error) {
this.handleError(error, 'primary');
if (fallbackFunction) {
try {
return fallbackFunction.call(context);
} catch (fallbackError) {
this.handleError(fallbackError, 'fallback');
return null;
}
}
return null;
}
}
handleError(error, type) {
this.errorCount++;
console.error(`エラー発生 (${type}):`, error);
// エラーレポート
this.reportError(error, type);
// エラー数が閾値を超えた場合、フォールバックモードに切り替え
if (this.errorCount >= this.maxErrors) {
this.enableFallbackMode();
}
}
reportError(error, type) {
let errorReport = {
message: error.message,
stack: error.stack,
type: type,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
};
// エラーレポートを外部システムに送信
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/error-report', JSON.stringify(errorReport));
}
}
enableFallbackMode() {
if (this.fallbackMode) return;
this.fallbackMode = true;
console.warn('フォールバックモードを有効化しました');
// 重要でない機能を無効化
this.disableNonCriticalFeatures();
}
disableNonCriticalFeatures() {
// アニメーションの無効化
document.body.classList.add('no-animations');
// 非同期処理の簡略化
document.body.classList.add('fallback-mode');
// 複雑なイベントハンドラーの無効化
this.simplifyEventHandlers();
}
simplifyEventHandlers() {
// 複雑なイベントハンドラーを簡単なものに置き換え
let elements = document.querySelectorAll('[data-complex-handler]');
elements.forEach(element => {
// 複雑なハンドラーを削除
element.removeEventListener('click', element.complexHandler);
// 簡単なハンドラーを追加
element.addEventListener('click', this.simpleClickHandler);
});
}
simpleClickHandler(event) {
// 最小限の処理のみ実行
let target = event.target;
let href = target.getAttribute('href') || target.dataset.href;
if (href) {
window.location.href = href;
}
}
}
実際の改善事例とケーススタディ
事例1: 大規模ECサイトの高速化
Copy// 実際のECサイトで実装された最適化
class ECommerceOptimizer {
constructor() {
this.productCache = new Map();
this.imageCache = new Map();
this.isOptimized = false;
}
async optimizeProductListing() {
// 商品一覧の最適化
let productContainer = document.querySelector('.product-list');
if (!productContainer) return;
// 初期表示は最初の20件のみ
let initialProducts = await this.loadProducts(0, 20);
this.renderProducts(initialProducts, productContainer);
// 無限スクロールの実装
this.setupInfiniteScroll(productContainer);
// 商品画像の遅延読み込み
this.setupLazyImageLoading();
}
async loadProducts(offset, limit) {
let cacheKey = `products-${offset}-${limit}`;
if (this.productCache.has(cacheKey)) {
return this.productCache.get(cacheKey);
}
try {
let response = await fetch(`/api/products?offset=${offset}&limit=${limit}`);
let products = await response.json();
// キャッシュに保存
this.productCache.set(cacheKey, products);
return products;
} catch (error) {
console.error('商品データの読み込みエラー:', error);
return [];
}
}
renderProducts(products, container) {
let fragment = document.createDocumentFragment();
products.forEach(product => {
let productElement = this.createProductElement(product);
fragment.appendChild(productElement);
});
container.appendChild(fragment);
}
createProductElement(product) {
let element = document.createElement('div');
element.className = 'product-item';
element.innerHTML = `
<div class="product-image">
<img data-src="${product.image}" alt="${product.name}" class="lazy-image">
</div>
<div class="product-info">
<h3>${product.name}</h3>
<p class="price">¥${product.price.toLocaleString()}</p>
<button class="add-to-cart" data-product-id="${product.id}">
カートに追加
</button>
</div>
`;
return element;
}
setupInfiniteScroll(container) {
let loading = false;
let currentOffset = 20;
let observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !loading) {
loading = true;
let products = await this.loadProducts(currentOffset, 20);
if (products.length > 0) {
this.renderProducts(products, container);
currentOffset += 20;
}
loading = false;
}
}, {
rootMargin: '100px'
});
// センチネル要素の作成
let sentinel = document.createElement('div');
sentinel.className = 'scroll-sentinel';
container.appendChild(sentinel);
observer.observe(sentinel);
}
setupLazyImageLoading() {
let imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
let img = entry.target;
let src = img.dataset.src;
if (src) {
img.src = src;
img.classList.add('loaded');
imageObserver.unobserve(img);
}
}
});
});
// 遅延読み込み対象の画像を監視
document.querySelectorAll('.lazy-image').forEach(img => {
imageObserver.observe(img);
});
// 動的に追加される画像も監視
let mutationObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) { // 要素ノード
let lazyImages = node.querySelectorAll('.lazy-image');
lazyImages.forEach(img => {
imageObserver.observe(img);
});
}
});
});
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
// 使用例
let optimizer = new ECommerceOptimizer();
optimizer.optimizeProductListing();
事例2: メディアサイトの記事読み込み最適化
Copy// メディアサイトの記事読み込み最適化
class MediaSiteOptimizer {
constructor() {
this.articleCache = new Map();
this.readingProgress = new Map();
this.analyticsQueue = [];
}
init() {
this.optimizeArticleLoading();
this.setupReadingAnalytics();
this.setupProgressiveEnhancement();
}
optimizeArticleLoading() {
// 記事のプリロード
this.preloadNextArticles();
// 関連記事の遅延読み込み
this.setupLazyRelatedArticles();
// 広告の最適化
this.optimizeAdLoading();
}
preloadNextArticles() {
let nextArticleLinks = document.querySelectorAll('.next-article-link');
nextArticleLinks.forEach(link => {
let href = link.getAttribute('href');
if (href) {
// リンクがビューポートに近づいたらプリロード
let observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.preloadArticle(href);
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '200px'
});
observer.observe(link);
}
});
}
async preloadArticle(url) {
if (this.articleCache.has(url)) return;
try {
let response = await fetch(url);
let html = await response.text();
// 記事の主要部分のみを抽出
let parser = new DOMParser();
let doc = parser.parseFromString(html, 'text/html');
let articleContent = doc.querySelector('.article-content');
if (articleContent) {
this.articleCache.set(url, articleContent.innerHTML);
}
} catch (error) {
console.error('記事のプリロードエラー:', error);
}
}
setupLazyRelatedArticles() {
let relatedContainer = document.querySelector('.related-articles');
if (!relatedContainer) return;
let observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
await this.loadRelatedArticles(relatedContainer);
observer.unobserve(entries[0].target);
}
}, {
rootMargin: '50px'
});
observer.observe(relatedContainer);
}
async loadRelatedArticles(container) {
let articleId = document.body.dataset.articleId;
try {
let response = await fetch(`/api/related-articles/${articleId}`);
let articles = await response.json();
let fragment = document.createDocumentFragment();
articles.forEach(article => {
let articleElement = this.createRelatedArticleElement(article);
fragment.appendChild(articleElement);
});
container.appendChild(fragment);
} catch (error) {
console.error('関連記事の読み込みエラー:', error);
}
}
createRelatedArticleElement(article) {
let element = document.createElement('article');
element.className = 'related-article';
element.innerHTML = `
<a href="${article.url}" class="article-link">
<div class="article-thumbnail">
<img data-src="${article.thumbnail}" alt="${article.title}" class="lazy-image">
</div>
<div class="article-info">
<h3>${article.title}</h3>
<p class="article-excerpt">${article.excerpt}</p>
<time class="article-date">${new Date(article.publishedAt).toLocaleDateString()}</time>
</div>
</a>
`;
return element;
}
setupReadingAnalytics() {
let article = document.querySelector('.article-content');
if (!article) return;
// 読了率の計算
this.trackReadingProgress(article);
// 滞在時間の計算
this.trackTimeOnPage();
// スクロール深度の計算
this.trackScrollDepth();
}
trackReadingProgress(article) {
let paragraphs = article.querySelectorAll('p');
let readParagraphs = new Set();
let observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
let index = Array.from(paragraphs).indexOf(entry.target);
readParagraphs.add(index);
let progress = (readParagraphs.size / paragraphs.length) * 100;
this.updateReadingProgress(progress);
}
});
}, {
threshold: 0.5
});
paragraphs.forEach(p => observer.observe(p));
}
updateReadingProgress(progress) {
let articleId = document.body.dataset.articleId;
this.readingProgress.set(articleId, progress);
// 25%, 50%, 75%, 100% の節目で分析データを送信
if (progress >= 25 && progress < 50) {
this.queueAnalytics('reading_progress', '25%');
} else if (progress >= 50 && progress < 75) {
this.queueAnalytics('reading_progress', '50%');
} else if (progress >= 75 && progress < 100) {
this.queueAnalytics('reading_progress', '75%');
} else if (progress >= 100) {
this.queueAnalytics('reading_progress', '100%');
}
}
trackTimeOnPage() {
let startTime = Date.now();
// ページ離脱時に滞在時間を送信
window.addEventListener('beforeunload', () => {
let timeOnPage = Date.now() - startTime;
this.queueAnalytics('time_on_page', Math.round(timeOnPage / 1000));
this.sendAnalytics();
});
}
trackScrollDepth() {
let maxScrollDepth = 0;
window.addEventListener('scroll', () => {
let scrollDepth = Math.round(
(window.pageYOffset + window.innerHeight) /
document.documentElement.scrollHeight * 100
);
maxScrollDepth = Math.max(maxScrollDepth, scrollDepth);
// スクロール深度の節目で分析データを送信
if (scrollDepth >= 90 && !this.scrollDepth90Sent) {
this.queueAnalytics('scroll_depth', '90%');
this.scrollDepth90Sent = true;
}
});
}
queueAnalytics(event, value) {
this.analyticsQueue.push({
event: event,
value: value,
timestamp: Date.now(),
url: window.location.href
});
// キューが一定数に達したら送信
if (this.analyticsQueue.length >= 5) {
this.sendAnalytics();
}
}
sendAnalytics() {
if (this.analyticsQueue.length === 0) return;
let data = JSON.stringify(this.analyticsQueue);
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics', data);
} else {
fetch('/api/analytics', {
method: 'POST',
body: data,
headers: {
'Content-Type': 'application/json'
}
});
}
this.analyticsQueue = [];
}
setupProgressiveEnhancement() {
// 基本機能が動作することを確認してから拡張機能を有効化
if (this.isBasicFunctionalityWorking()) {
this.enableEnhancedFeatures();
}
}
isBasicFunctionalityWorking() {
// 基本的な機能のチェック
return (
'querySelector' in document &&
'addEventListener' in window &&
'fetch' in window
);
}
enableEnhancedFeatures() {
// 拡張機能の有効化
this.setupSmoothScrolling();
this.setupImageZoom();
this.setupSocialSharing();
}
setupSmoothScrolling() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', (e) => {
e.preventDefault();
let target = document.querySelector(anchor.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
}
setupImageZoom() {
let images = document.querySelectorAll('.article-content img');
images.forEach(img => {
img.addEventListener('click', (e) => {
this.showImageModal(e.target);
});
img.style.cursor = 'zoom-in';
});
}
showImageModal(img) {
let modal = document.createElement('div');
modal.className = 'image-modal';
modal.innerHTML = `
<div class="modal-background"></div>
<div class="modal-content">
<img src="${img.src}" alt="${img.alt}">
<button class="modal-close">×</button>
</div>
`;
document.body.appendChild(modal);
// モーダルを閉じる処理
modal.addEventListener('click', (e) => {
if (e.target === modal || e.target.classList.contains('modal-close')) {
modal.remove();
}
});
}
setupSocialSharing() {
let shareButtons = document.querySelectorAll('.share-button');
shareButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
let platform = button.dataset.platform;
let url = encodeURIComponent(window.location.href);
let title = encodeURIComponent(document.title);
let shareUrl = this.getShareUrl(platform, url, title);
if (shareUrl) {
window.open(shareUrl, '_blank', 'width=600,height=400');
}
});
});
}
getShareUrl(platform, url, title) {
const shareUrls = {
twitter: `https://twitter.com/intent/tweet?url=${url}&text=${title}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${url}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${url}`
};
return shareUrls[platform];
}
}
// 使用例
let mediaOptimizer = new MediaSiteOptimizer();
mediaOptimizer.init();
まとめ
JavaScript の高速化は、現代のウェブ開発において避けて通れない重要な技術です。本記事で紹介した最適化テクニックを適切に適用することで、ユーザーエクスペリエンスを大幅に改善し、ビジネス成果の向上につなげることができます。
重要なポイントの振り返り
- 基本的な最適化の徹底
- ガベージコレクションの回避
- メモリリークの防止
- DOM操作の最適化
- 適切な読み込み戦略
- defer属性による非同期読み込み
- 適切なタイミングでのasync使用
- ファーストビューの最適化
- 高度な最適化技術
- Web Workersの活用
- Service Workerによるキャッシュ戦略
- 非同期処理の最適化
- 継続的な改善
- パフォーマンスの監視
- 段階的な最適化アプローチ
- エラーハンドリングとフォールバック
ランディングページ制作における活用
特にランディングページでは、表示速度がコンバージョン率に直結するため、これらの最適化技術の重要性は更に高まります。LandingHubのようなツールを使用する際も、JavaScript の最適化を意識することで、より効果的なランディングページを構築できます。
表示速度の改善は一朝一夕には実現できませんが、本記事で紹介した手法を段階的に適用することで、確実にパフォーマンスを向上させることができます。定期的な測定と改善を継続し、ユーザーに最高のエクスペリエンスを提供しましょう。