JavaScriptの速度改善方法!高速化テクニックを公開

JavaScriptの速度改善方法!高速化テクニックを公開

JavaScriptが重くてサイトが遅い!そんな悩みを抱えていませんか?

正しい知識と手順さえ知っていれば、誰でも大幅な改善が可能です。

本記事では、async/deferの使い分け、DOM操作の最適化まで、速度改善、高速化テクニックを徹底解説します。

JavaScriptがページ速度に与える影響

JavaScriptはページ速度に3つの重大な影響を与えます。

1つはレンダリングブロッキングによる影響です。

ブラウザはJavaScriptを発見するとHTMLの解析を停止します。特に<head>内のスクリプトはページ初期表示を大幅に遅延させます。

2つ目はメインスレッドの占有による影響も大きいです。

JavaScript実行中、ブラウザは他の処理ができません。重い計算や大量のDOM操作はページをフリーズさせます。

3つ目にファイルサイズの負荷も影響があります。

大きなファイルは、ダウンロード→パース→コンパイル→実行の各段階で時間を消費。モバイルでは特に影響が大きくなります。

JavaScriptの速度改善が重要な理由

JavaScript速度改善が重要なポイントは、ビジネス成果への影響です。

サイトの表示速度が遅くなると、コンバージョン率が低下し離脱率も増加します。

これは、売り上げにも直結する要素でビジネス面で大きな影響を与えます。

次に挙げられる要素として、SEOへの評価もあります。

GoogleのCore Web Vitalsで速度指標が検索順位に影響し、遅いサイトは上位表示されにくくなります。

新規流入や検索からのアクセスが下がることになり、こちらもビジネス面で大きな影響を与えるでしょう。

JavaScriptはブラウザのメインスレッドをブロックし、レンダリングを妨げます。

最適化しないと、HTMLやCSSの処理も遅延し、ページ全体のパフォーマンスが低下します。

JavaScriptの速度改善方法

JavaScript速度改善は3段階で進めます。

第1段階では、async/defer属性の追加、未使用コードの削除、ファイル圧縮を実施。

第2段階では、DOM操作のバッチ化、Promise.allによる並列処理、イベント最適化に取り組みます。

第3段階では、Web Workers、Code Splitting、Resource Hintsを活用。

各段階でPageSpeed Insightsなどのツールで効果を測定しながら進めることで、確実なスコア改善が可能です。

async/defer属性で最適化をかける

JavaScript最適化のおすすめの方法として、async/defer属性で最適化をかける方法があります。

それぞれ解説していきます。

async属性を使用した非同期読み込み

asyncキーワードを使用すると、JavaScriptファイルの読み込みが非同期で行われます。

これにより、HTMLの解析とJavaScriptの読み込みが並行して実行され、ページの表示速度が向上します。

<script src="example.js" async></script>

特にファーストビューなどすぐに読み込みが必要な場合に使用されることが多いです。

ただし、注意点があります。

asyncで読み込まれたスクリプトは、読み込みが完了したタイミングで即座に実行されるため、複数のスクリプト間で依存関係がある場合、実行順序が保証されません。

deferキーワードを使用した遅延実行

deferキーワードは、asyncと同様に非同期読み込みを行いますが、スクリプトの実行タイミングがHTMLの解析完了後(DOMContentLoadedイベント発生前)に遅延されます。

<script src="jquery.js" defer></script>
<script src="main.js" defer></script>

この場合、複数のスクリプトがある場合でも、HTML内での記述順序で実行されるため、依存関係のあるライブラリを使用している場合も安全です。

未使用JavaScriptの削除

未使用JavaScriptとは、ページで実際に実行されていないコードです。

ライブラリ全体を読み込んでいるが一部しか使っていない、過去に使用していたが現在は不要、デバッグ用コードが残っているなどのケースが該当します。

未使用コードはファイルサイズを増大させ、ダウンロード、パース、コンパイルの時間を無駄に消費します。

削除することでファイルサイズが削減され、表示速度が大幅に改善します。

Chrome DevToolsで未使用コードを特定し。削除を進めましょう。

JavaScriptファイル圧縮を実施

ファイル圧縮(Minify)とは、JavaScriptコードから空白、改行、コメント、不要な文字を削除し、変数名を短縮することでファイルサイズを削減する手法です。

圧縮により、ファイルサイズが削減されます。

以下の方法で、ファイルサイズを大幅に削減できます。

Minification(圧縮)

不要な空白や改行、コメントを削除し、変数名を短縮することで、ファイルサイズを30-50%削減できます。

Gzip圧縮

サーバーレベルでの圧縮により、転送時のファイルサイズを60-80%削減できます。

# Apache設定例
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE application/javascript
  AddOutputFilterByType DEFLATE text/javascript
</IfModule>

# Nginx設定例
gzip on;
gzip_types application/javascript text/javascript;

Tree Shaking

使用されていないコードを自動的に削除する技術で、WebpackやRollupなどのビルドツールで実装できます。

例えば100KBのファイルが50KBになれば、ダウンロード時間が半分になり、特にモバイル環境で効果が顕著です。

パース時間も短縮され、ページ表示速度が大幅に改善します。

実装にはWebpackやViteなどのビルドツールを使用し、本番環境用のビルド時に自動圧縮する設定が一般的です。

圧縮後も機能は完全に保たれ、開発時は読みやすい元のコードで作業できます。

PageSpeed Insightsでも推奨される基本的な最適化施策です。

DOM操作のバッチ化

DOM操作のバッチ化とは、複数のDOM操作をまとめて一度に実行する最適化手法です。

ブラウザはDOM操作ごとにレイアウト計算と再描画を行うため、操作が多いほど処理時間が増大します。

例えば、1000個の要素を1つずつ追加すると1000回の再描画が発生しますが、DocumentFragmentを使いまとめて追加すれば1回で済み、処理速度が数十倍に改善します。

// 悪い例:複数回DOM操作
for (let i = 0; i < 1000; i++) {
  element.innerHTML += '<div>item</div>';
}

// 良い例:1回で処理
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = 'item';
  fragment.appendChild(div);
}
element.appendChild(fragment);

実装はDocumentFragmentに要素を追加し、最後に一度だけDOMに挿入します。

リスト表示やテーブル生成など大量要素を扱う場合に特に効果的で、体感速度が劇的に向上します。

Promise.allによる並列処理

romise.allは、複数の非同期処理を並列実行し、全ての処理が完了するまで待機する関数です。

通常のawaitで順次実行すると、各処理の待ち時間が累積しますが、Promise.allを使えば同時に実行されるため、総処理時間が大幅に短縮されます。

// 順次実行(遅い)
const res1 = await fetch('/api/1');
const res2 = await fetch('/api/2');

// 並列実行(速い)
const [res1, res2] = await Promise.all([
  fetch('/api/1'),
  fetch('/api/2')
]);

例えば、3つのAPI呼び出しをそれぞれ1秒かかるとします。

順次実行では合計3秒必要ですが、Promise.allなら並列実行されるため約1秒で完了します。

ただし、1つでも失敗すると全体が失敗扱いになるため、エラーハンドリングが重要です。

個別の成功・失敗を判定したい場合はPromise.allSettledを使用します。

互いに依存しない複数のAPI呼び出しやデータ取得で効果絶大です。

イベント最適化

イベント最適化とは、イベントリスナーの登録方法や実行頻度を改善する手法です。主に3つの方法があります。

イベントデリゲーションは、多数の要素に個別リスナーを登録せず、親要素1つにまとめることでメモリ消費とDOM操作を削減します。

// (悪い例:個別にリスナーを登録)1000個の要素にそれぞれリスナーを登録
const items = document.querySelectorAll('.list-item');

items.forEach(item => {
  item.addEventListener('click', function(e) {
    console.log('クリックされた:', this.textContent);
    this.classList.toggle('active');
  });
});

// 問題点:
// - 1000個のイベントリスナーを登録(メモリ消費大)
// - 動的に追加された要素にはイベントが効かない
// - パフォーマンスが低下


// (良い例:イベントデリゲーション)親要素に1つだけリスナーを登録
const list = document.querySelector('.list');

list.addEventListener('click', function(e) {
  // クリックされた要素が.list-itemか確認
  const item = e.target.closest('.list-item');
  
  if (item) {
    console.log('クリックされた:', item.textContent);
    item.classList.toggle('active');
  }
});

// メリット:
// - リスナーは1つだけ(メモリ効率的)
// - 動的に追加された要素も自動的に対応
// - パフォーマンスが大幅に向上

デバウンス・スロットルは、スクロールやリサイズなど高頻度イベントの実行回数を制限し、処理負荷を軽減します。

Passive Event Listenerは、addEventListener のpassiveオプションでブラウザのスクロール処理をブロックせず、スムーズな操作を実現します。

これらの最適化により、大量要素を扱うリストやスクロール処理で劇的な改善効果が得られます。

Web Workersを活用

Web Workersは、JavaScriptを別スレッドで実行し、メインスレッドをブロックせずに重い処理を実行する技術です。

通常のJavaScriptはシングルスレッドのため、重い計算中はUIがフリーズしますが、Workersを使えば解決できます。

画像処理、大量データ計算、複雑なソート処理などをWorkerに任せることで、メインスレッドはUI操作に専念でき、処理中もスムーズな操作が可能になります。

// image-worker.js
self.addEventListener('message', function(e) {
  const imageData = e.data;
  const pixels = imageData.data;
  
  // グレースケール変換
  for (let i = 0; i < pixels.length; i += 4) {
    const gray = pixels[i] * 0.299 + 
                 pixels[i + 1] * 0.587 + 
                 pixels[i + 2] * 0.114;
    pixels[i] = pixels[i + 1] = pixels[i + 2] = gray;
  }
  
  // 結果を返す
  self.postMessage(imageData, [imageData.data.buffer]);
  // Transferable Objects使用でコピーコスト削減
});

// main.js
const imageWorker = new Worker('image-worker.js');

imageWorker.addEventListener('message', function(e) {
  const processed = e.data;
  ctx.putImageData(processed, 0, 0);
  console.log('処理完了');
});

button.addEventListener('click', function() {
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  imageWorker.postMessage(imageData, [imageData.data.buffer]);
  // UIはブロックされず、他の操作が可能
  console.log('処理中でもこのログが即座に表示される');
});

ただしWorkerからはDOMアクセスができず、計算処理専用です。

データはpostMessageで送受信し、大容量データにはTransferable Objectsを使用します。

Code Splittingを活用

Code Splittingは、JavaScriptを複数ファイルに分割し、必要な時だけ読み込む最適化手法です。

全コードを最初に読み込まず、初期表示に必要な部分だけ先に読み込むことで、表示速度が大幅に向上します。

Code Splitting活用前のコード例

// 全てを1つのバンドルに含める
import Home from './pages/Home';
import About from './pages/About';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
import HeavyChart from './components/HeavyChart';
import VideoPlayer from './components/VideoPlayer';

// 結果:bundle.js(500KB)
// 問題:ユーザーがHomeページしか見なくても全て読み込む

下記のようにファイルを分割しておき、すぐ読み込みが必要なものと、そうでないものを分けます。

Code Splitting活用のコード例

// 必要な時だけ読み込む
// 初期バンドル:home.js(50KB)のみ

// 他のページは遷移時に読み込む
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

// 結果:初期読み込みが90%削減

例えば100KBのアプリを初期用30KBと機能別70KBに分割すれば、初期読み込み時間が70%短縮されます。

SPAや大規模アプリで特に効果的で、LCPスコアが劇的に改善します。

実装にはDynamic Import、React.lazy、Webpackの分割機能を使用します。

ルート単位やコンポーネント単位で分割することで、初期バンドルサイズを削減し、PageSpeed Insightsのスコアを大幅に改善できます。

Resource Hintsを活用

Resource Hintsは、ブラウザに次に必要なリソースを事前通知し、先読みや事前接続を行う最適化手法で主に4種類あります。

dns-prefetchはDNS解決のみ先実行する軽量版です。

外部ドメインのDNS解決を事前に実行し、実際のリクエスト時の待ち時間を削減します。

dns-prefetchソース例

<!-- 外部ドメインのDNS解決を先行 -->
<head>
  <link rel="dns-prefetch" href="https://fonts.googleapis.com">
  <link rel="dns-prefetch" href="https://cdn.example.com">
  <link rel="dns-prefetch" href="https://www.google-analytics.com">
  <link rel="dns-prefetch" href="https://api.example.com">
</head>

preconnectは外部ドメインへの接続を事前確立し、DNS解決とTLS接続時間を短縮します。

DNS解決、TCP接続、TLS/SSLハンドシェイクを事前に完了させます。dns-prefetchより包括的で効果が高いですが、コストも高くなります。

preconnectソース例

<head>
  <!-- 重要な外部ドメインへの完全な事前接続 -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link rel="preconnect" href="https://api.example.com">
</head>

preloadは重要リソースを優先読み込みし、LCP改善に効果的です。

preloadソース例

<head>
  <!-- JavaScript -->
  <link rel="preload" href="/js/critical.js" as="script">
  <!-- CSS -->
  <link rel="preload" href="/css/main.css" as="style">
  <!-- フォント -->
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
  <!-- 画像 -->
  <link rel="preload" href="/images/hero.jpg" as="image">
  <!-- 動画 -->
  <link rel="preload" href="/videos/intro.mp4" as="video">
</head>

prefetchは次ページで必要なリソースをアイドル時に先読みし、ページ遷移を高速化します。

適切な使用で外部API応答が200〜500ms短縮され、ページ遷移が体感的に瞬時になります。CDNや外部サービス利用サイトで特に効果的です。

JavaScriptの速度改善方法まとめ

JavaScript速度改善は、測定から始まり継続的な最適化が必要です。

本記事で紹介した手法は、いずれも実証済みの効果的な施策ですが、全てを一度に実装する必要はありません。

まずPageSpeed Insightsで現状を測定し、サイトの特性とユーザー行動に合わせて優先順位をつけましょう。

初級の基本施策だけでも、大きな改善が期待できます。

速度改善の本質は、ユーザーに快適な体験を提供することです。1秒の短縮が、離脱率の低下とコンバージョン率の向上につながります。

完璧を目指すのではなく、継続的な改善を心がけてください。小さな一歩が大きな成果を生み出します。

記事を書いた人

井上寛生

井上寛生

LandingHub 執行役員 / 事業責任者 / 技術責任者

大学院では情報工学を専攻し、修了後に株式会社TeNへ新卒入社。当時は社内唯一のエンジニアながら、開発部門をゼロから立ち上げ、採用・育成を一手に担い、全員が未経験からスタートした精鋭エンジニアチームを組成。2021 年にはWEBサイト高速化プラットフォーム「LandingHub」を立ち上げ、プロダクトオーナー兼事業責任者として企画・開発・グロースを牽引。現在は執行役員として、会社の技術戦略と事業成長の双方をリードしている。
コラム一覧に戻る