ページ表示の高速化は手法が多く、どこから手を付ければよいか迷いがちです。しかし整理すると、やるべきことは大きく2つの軸に分けられます。「配信を軽くする(読み込みを速く)」と「実行を速くする(動作を軽く)」です。
この記事では、その2軸に沿って、現在(HTTP/2やモダンビルドツールが前提の時代)に有効な手法だけを、実例コードと計測方法とあわせて解説します。
defer・キャッシュ・モダンバンドラ)と、②実行を速くする(DOM操作削減・debounce/throttle・遅延読み込み・Web Worker)の2軸で考えます。まず計測してボトルネックを特定し、効く所から直すのが鉄則です。配信を軽くする(読み込みを速くする)
ブラウザがJavaScriptを受け取って実行できるまでの時間を短くする工夫です。ファイルを小さく・賢く届けることが目的です。
minify(圧縮)でファイルを小さくする
不要な空白・改行・コメントを削り、変数名を短縮してファイルサイズを減らします。現在のモダンJS(ES6+)では Terser がデファクトです(古い UglifyJS はES6+を扱えないため非推奨)。とはいえ、後述のバンドラを使えば本番ビルドで自動的にminifyされるため、個別にツールを叩く機会はほとんどありません。
「結合」ではなく「コード分割」する
現在の正解は逆で、コード分割(Code Splitting)です。最初に必要な分だけ読み込み、残りはページ遷移や操作のタイミングで遅延読み込みします。モダンバンドラなら動的 import() で簡単に分割できます。
button.addEventListener("click", async () => {
// クリックされて初めてモジュールを取得(初期表示には含めない)
const { showChart } = await import("./chart.js");
showChart();
});
async / defer で読み込みをブロックしない
<script> を普通に書くとHTMLの解析が止まります。defer や async を付けると、解析を止めずに読み込めます。
<!-- defer: 解析を止めず、HTMLパース後に「記述順どおり」実行(基本はこれ) --> <script src="app.js" defer></script> <!-- async: ダウンロード完了次第すぐ実行、順序は保証されない(独立した計測タグ等向け) --> <script src="analytics.js" async></script>
他のスクリプトに依存するアプリ本体はdefer、どこで実行されても困らない独立スクリプトは async、と使い分けます。
キャッシュを効かせる(ファイル名ハッシュ)
一度ダウンロードしたJSを再訪問時に再取得しないよう、HTTPヘッダーで長期キャッシュを指定します。ただし「更新したのに古いJSが使われる」事故を防ぐため、app.3f9a2c.js のようにファイル名に内容ハッシュを付けるのが定番です。内容が変わればファイル名も変わるので、確実に新しいファイルが読まれます。これもモダンバンドラが自動で付与します。
モジュールバンドラを使う(Vite / esbuild)
ここまでの「minify・コード分割・ハッシュ付与」をまとめて自動化するのがバンドラです。現在の主流は次のとおりです。
- Vite:
esbuildとRollupを内部利用する高速ツール。現在の事実上の標準。 - esbuild:Go製で非常に高速。ビルドだけ欲しいときに。
- Webpack:歴史が長く資料も豊富。既存プロジェクトで現役。
新規ならまず Vite を選んでおけば、本番ビルドで最適化が自動的にかかります。
実行を速くする(動作を軽くする)
読み込み後、JavaScriptが動くときの「カクつき」を減らす工夫です。体感速度に直結するため、配信の最適化と同じくらい重要です。
不要なDOM操作を減らす
DOMの更新はコストが高く、ループ内で繰り返すと描画が何度も走って重くなります。DocumentFragment にまとめてから1回だけ追加するのが基本です。
const list = document.getElementById("list");
// NG: ループのたびにDOMを更新=毎回レイアウトが走って重い
// for (const item of items) list.innerHTML += `<li>${item}</li>`;
// OK: フラグメントに組み立て、最後に一度だけ追加する
const frag = document.createDocumentFragment();
for (const item of items) {
const li = document.createElement("li");
li.textContent = item; // textContentでXSSも防ぐ
frag.appendChild(li);
}
list.appendChild(frag); // DOM更新はこの1回だけ
要素の取得方法はquerySelectorでDOM要素を取得する方法を参考にしてください。
イベントを間引く(debounce / throttle)
scroll や resize、入力イベントは短時間に何十回も発火します。毎回重い処理を走らせると一気に重くなるため、debounce(最後の1回だけ)やthrottle(一定間隔に1回)で間引きます。
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// リサイズが「止まってから」200ms後に1回だけ実行
window.addEventListener("resize", debounce(() => {
console.log("レイアウト再計算");
}, 200));
2つの違いと使い分け、throttleの実装はdebounceとthrottleの違いと実装方法で詳しく解説しています。
画像・コンテンツを遅延読み込みする
初期表示に不要な画像は、画面に入ってから読み込むことで初期表示が軽くなります。画像なら loading="lazy" を付けるだけ、細かく制御するなら IntersectionObserver を使います。
<img src="photo.jpg" loading="lazy" alt="説明">
実装の詳細やLCP(表示速度指標)への影響はLazyload(遅延読み込み)完全ガイドにまとめています。
アニメーションは requestAnimationFrame で
位置やサイズを連続して動かすアニメーションを setInterval で書くとカクつきます。ブラウザの描画に同期する requestAnimationFrame を使うと滑らかになり、タブが非表示のときは自動で止まって無駄も省けます。具体例はページを自動スクロールさせる方法を参照してください。
重い処理は Web Worker に逃がす
大量データの計算などをメインスレッドで実行すると、その間UIが固まります。Web Worker で別スレッドに逃がせば、画面を止めずに処理できます。
// メイン側
const worker = new Worker("worker.js");
worker.postMessage(largeData); // 処理を依頼
worker.onmessage = (e) => {
console.log("結果:", e.data); // 完了通知を受け取る
};
// worker.js(別ファイル)
onmessage = (e) => {
const result = heavyCompute(e.data); // メインスレッドを止めない
postMessage(result);
};
非同期I/Oは async / await で待つ
通信などの待ち時間でメインスレッドを占有しないよう、非同期処理は async/await で書きます。基本から実務パターンまではPromiseとasync/awaitの使い方で解説しています。
まず計測してボトルネックを特定する
やみくもに最適化しても効果は薄いです。計測 → 一番遅い所を直す → 再計測の順で進めます。
- Lighthouse / DevToolsのPerformanceタブ:ページ全体のボトルネックを可視化
performance.now():特定処理の所要時間をミリ秒で計測
const t0 = performance.now();
heavyTask();
const t1 = performance.now();
console.log(`処理時間: ${(t1 - t0).toFixed(1)} ms`);
// 手軽に測るなら console.time / timeEnd でも可
console.time("task");
heavyTask();
console.timeEnd("task");
よくある質問(FAQ)
defer・キャッシュ)と「実行を速くする」(不要なDOM操作の削減、debounce/throttle、画像のLazy Load、Web Worker、requestAnimationFrame)の2軸があります。まず計測してボトルネックから直します。defer です。HTMLの解析を止めず、記述順どおりに実行されるため依存関係が崩れません。async は実行順序が保証されないので、他に依存しない独立したスクリプト(計測タグなど)に向いています。setTimeout(fn, 0) や requestIdleCallback で遅延実行し、通信などの待機は async/await でメインスレッドを解放します。performance.now() で処理時間を計測します。console.time() と console.timeEnd() を使えば手軽に測れます。まとめ
ページ表示の高速化は、①配信を軽くする(minify・コード分割・defer・キャッシュ・モダンバンドラ)と、②実行を速くする(DOM操作削減・debounce/throttle・遅延読み込み・Web Worker)の2軸で考えると整理できます。
注意したいのは、「ファイルの結合」のような古い常識はHTTP/2時代には逆効果になり得る点です。まずは performance.now() やLighthouseで計測してボトルネックを特定し、効果の大きい所から手を入れていきましょう。
