【JavaScript】ページ表示を高速化する方法|配信と実行の最適化

ページ表示の高速化は手法が多く、どこから手を付ければよいか迷いがちです。しかし整理すると、やるべきことは大きく2つの軸に分けられます。「配信を軽くする(読み込みを速く)」「実行を速くする(動作を軽く)」です。

この記事では、その2軸に沿って、現在(HTTP/2やモダンビルドツールが前提の時代)に有効な手法だけを、実例コードと計測方法とあわせて解説します。

この記事の結論:高速化は①配信を軽くする(minify・コード分割・defer・キャッシュ・モダンバンドラ)と、②実行を速くする(DOM操作削減・debounce/throttle・遅延読み込み・Web Worker)の2軸で考えます。まず計測してボトルネックを特定し、効く所から直すのが鉄則です。
スポンサーリンク

配信を軽くする(読み込みを速くする)

ブラウザがJavaScriptを受け取って実行できるまでの時間を短くする工夫です。ファイルを小さく・賢く届けることが目的です。

minify(圧縮)でファイルを小さくする

不要な空白・改行・コメントを削り、変数名を短縮してファイルサイズを減らします。現在のモダンJS(ES6+)では Terser がデファクトです(古い UglifyJS はES6+を扱えないため非推奨)。とはいえ、後述のバンドラを使えば本番ビルドで自動的にminifyされるため、個別にツールを叩く機会はほとんどありません。

「結合」ではなく「コード分割」する

古い常識に注意:かつては複数のJSを1つに結合してHTTPリクエスト数を減らすのが定石でした。しかしHTTP/2以降は1本の接続で複数ファイルを並行ダウンロードできるため、結合の利点はほぼ消えています。むしろ巨大な1ファイルは「1行直すだけで全体のキャッシュが無効化」されて逆効果です。

現在の正解は逆で、コード分割(Code Splitting)です。最初に必要な分だけ読み込み、残りはページ遷移や操作のタイミングで遅延読み込みします。モダンバンドラなら動的 import() で簡単に分割できます。

JavaScript:必要になった時だけ読み込む(動的import)
button.addEventListener("click", async () => {
  // クリックされて初めてモジュールを取得(初期表示には含めない)
  const { showChart } = await import("./chart.js");
  showChart();
});

async / defer で読み込みをブロックしない

<script> を普通に書くとHTMLの解析が止まります。deferasync を付けると、解析を止めずに読み込めます。

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・コード分割・ハッシュ付与」をまとめて自動化するのがバンドラです。現在の主流は次のとおりです。

  • ViteesbuildRollup を内部利用する高速ツール。現在の事実上の標準。
  • esbuild:Go製で非常に高速。ビルドだけ欲しいときに。
  • Webpack:歴史が長く資料も豊富。既存プロジェクトで現役。

新規ならまず Vite を選んでおけば、本番ビルドで最適化が自動的にかかります。

実行を速くする(動作を軽くする)

読み込み後、JavaScriptが動くときの「カクつき」を減らす工夫です。体感速度に直結するため、配信の最適化と同じくらい重要です。

不要なDOM操作を減らす

DOMの更新はコストが高く、ループ内で繰り返すと描画が何度も走って重くなります。DocumentFragment にまとめてから1回だけ追加するのが基本です。

JavaScript:DOM更新は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)

scrollresize、入力イベントは短時間に何十回も発火します。毎回重い処理を走らせると一気に重くなるため、debounce(最後の1回だけ)throttle(一定間隔に1回)で間引きます。

JavaScript:debounceで間引く
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 を使います。

HTML:画像の遅延読み込み
<img src="photo.jpg" loading="lazy" alt="説明">

実装の詳細やLCP(表示速度指標)への影響はLazyload(遅延読み込み)完全ガイドにまとめています。

アニメーションは requestAnimationFrame で

位置やサイズを連続して動かすアニメーションを setInterval で書くとカクつきます。ブラウザの描画に同期する requestAnimationFrame を使うと滑らかになり、タブが非表示のときは自動で止まって無駄も省けます。具体例はページを自動スクロールさせる方法を参照してください。

重い処理は Web Worker に逃がす

大量データの計算などをメインスレッドで実行すると、その間UIが固まります。Web Worker で別スレッドに逃がせば、画面を止めずに処理できます。

JavaScript: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():特定処理の所要時間をミリ秒で計測
JavaScript:処理時間を計測する
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)

QJavaScriptでページのパフォーマンスを改善する主な方法は?
A大きく「配信を軽くする」(minify・コード分割・defer・キャッシュ)と「実行を速くする」(不要なDOM操作の削減、debounce/throttle、画像のLazy Load、Web Worker、requestAnimationFrame)の2軸があります。まず計測してボトルネックから直します。
Qasync と defer はどちらを使えばいい?
A基本は defer です。HTMLの解析を止めず、記述順どおりに実行されるため依存関係が崩れません。async は実行順序が保証されないので、他に依存しない独立したスクリプト(計測タグなど)に向いています。
QUIをブロックせずに重い処理を実行するには?
A重い計算は Web Worker に移して別スレッドで実行します。小さく分割するなら setTimeout(fn, 0)requestIdleCallback で遅延実行し、通信などの待機は async/await でメインスレッドを解放します。
QJavaScriptのパフォーマンスを測定するには?
ADevToolsのPerformanceタブやLighthouseでボトルネックを特定し、performance.now() で処理時間を計測します。console.time()console.timeEnd() を使えば手軽に測れます。

まとめ

ページ表示の高速化は、①配信を軽くする(minify・コード分割・defer・キャッシュ・モダンバンドラ)と、②実行を速くする(DOM操作削減・debounce/throttle・遅延読み込み・Web Worker)の2軸で考えると整理できます。

注意したいのは、「ファイルの結合」のような古い常識はHTTP/2時代には逆効果になり得る点です。まずは performance.now() やLighthouseで計測してボトルネックを特定し、効果の大きい所から手を入れていきましょう。