「JavaScriptの実行にかかる時間の低減」を改善する方法

「JavaScriptの実行にかかる時間の低減」を改善する方法

PageSpeed Insightsで「JavaScriptの実行にかかる時間の低減」と表示されたら、原因は“重いJSがメインスレッドを占有している”ことです。

本記事では、Lighthouseで遅いスクリプトを特定し、不要JSの削減、遅延読み込み、コード分割、ロングタスク分割、Web Worker活用まで、改善手順をそのまま実行できる形で解説します。

JavaScriptの実行にかかる時間の低減とは?

「JavaScript の実行にかかる時間の低減」とは、PageSpeed Insightsで、ページ読み込み中にJavaScriptの解析・コンパイル・評価に時間がかかり、メインスレッドを長く占有している状態を指します。

実行が重いと描画やユーザー操作への応答が遅れ、体感速度(特にTBTなど)に悪影響が出ます。

PageSpeed InsightsでURLを分析すると「パフォーマンスの問題を診断する>インサイト>TBT」の箇所に表示されます。

上記画像の黄色い部分でTBTをクリックしてあげると、絞り込まれて探しやすくなります。

赤い部分で全体の実行にかかっている時間が表示されています。

また、青い部分ではそれぞれどの項目でどのくらい時間がかかっているかがわかります。

JavaScript の解析、コンパイル、実行にかかる時間の短縮をご検討ください。JavaScript ペイロードのサイズを抑えるなどの方法があります。

といった表記で解説されていて、読み込み中にブラウザがJavaScriptの処理に時間を使いすぎているので、その時間を短くする改善を検討してくださいという指摘になります。

それぞれの項目の概要を下記で説明していきます。

合計CPU時間

合計CPU時間は、そのスクリプトがページ読み込み中に“CPU(主にメインスレッド)をどれだけ使ったか”の合計時間を指します。

ここには JSの解析+コンパイル+実行だけでなく、JSが引き金になって発生するレイアウト計算・レンダリング・HTML解析・ユーザー処理などの周辺コストも含まれます。

スクリプトの評価

「スクリプトの評価」は、ブラウザがダウンロード済みのJavaScriptを実際に動かすために処理する時間のことです。

具体的には、読み込んだJSを解析→コンパイル→実行する一連の中でも、とくに「コードを走らせて初期化処理などを実行している時間」を指して表示されることが多いです。

ポイントは、評価が重いとメインスレッドが長時間ふさがり、ロングタスク(50ms超)になって操作遅延やTBT悪化につながりやすいことです。

スクリプトの解析

スクリプトの解析とは、ブラウザがダウンロードしたJavaScriptを実行する前に、コードを読み取って構文を確認し、内部的に扱える形へ変換する処理にかかる時間です。

解析の後にはコンパイルが続き、最終的に評価(実行)されます。

解析時間が長い原因は、JSファイルが大きい/不要コードが多い/外部ライブラリ過多などが典型で、対策はペイロード削減やコード分割が有効です。

SEOやUXに有効な理由

JavaScriptはスタイル計算・レイアウト・描画と同じメインスレッドで動くため、長時間実行されるとフレーム落ちや操作不能を起こしやすくなります。

さらに、50msを超えるロングタスクが増えると、ユーザー操作への反応がブロックされる時間を示すTBTが悪化します。

TBTは「FCP〜TTIの間に発生したロングタスクの“50ms超過分”の合計」で、数値が大きいほど操作が効かない時間が増える=UX低下につながります。

SEO視点からしても、UXは大事な指標となっているため評価も上がりやすいサイトになります。

参考記事:TBTとは

重い原因になっているスクリプトを特定する方法

重いスクリプトを特定する手順は、下記のようになります。

  1. PageSpeed Insightsで「重いスクリプト一覧」を出す
  2. DevToolsで「どの処理(Evaluate Script等)が重いか」を確定
  3. 必要なら3rd partyをブロックして影響度を測る

の3段階となります。それぞれ解説していきます。

PageSpeed Insightsで「重いスクリプト一覧」を出す

PageSpeed InsightsでURLを計測し、「JavaScript の実行にかかる時間の低減」を開きます。

まずはここから対象となるスクリプトの一覧を出しておきましょう。

DevToolsで重くなっている箇所を探す

下記画像を参考に、DevToolsのパフォーマンス(赤枠)を開き、レコードボタン(黄色枠)を押します。

レコードボタンを押すと計測が始まります。

サイト全体が読み込めたら停止ボタンを押します。(よっぽど重いページでなければ10〜15秒もあれば十分かと思います)

※任意ではありますが、ネットワーク>スロットリングを低速4Gにして計測をお勧めします。

計測ができたら下記の画像のような画面になります。

赤枠の部分で赤いラインが出ているところ(ロングタスク)に幅を合わせると探しやすいです。

画像青枠のメインの部分で、タスクが赤くなっているところを探し、その下部にあるスクリプトの評価をクリックします。

すると画面黄色枠のところに対象となるアクティビティが出てきます。

ここで合計時間でソートをかけると、時間がかかっている順にファイルが出てきます。

ここで、ロングタスクの要因になっているファイルと、読み込みにかかっている時間が確認できます。

JavaScriptの実行にかかる時間の低減の改善方法

上記で実行2時間のかかるJavaScriptの対象ファイルを探せたら、次は改善する施策を実施していきます。

下記に改善パターンを紹介していきますので、適切に判断して実行していきましょう。

不要なJavaScriptを減らす

一番JavaScriptを削除できれば、効果が大きいです。

ただし、なんでも削除すればOKとはならず、JavaScriptを削除してOKかどうかを判断していく必要があります。

ネットワークでロングタスクを探し、時間のかかっているファイルを探せたらまずはそのファイル自体削除して大丈夫か?を判断します。

全く使用されていないファイルであれば削除しましょう。

削除できない場合は、ソースの中身を確認していきます。

上記の画像例だと、スタイルの再計算の項目に時間がかかっているため、このURLをクリックします。

するとファイル内のソースが表示されるので、中身をみていきましょう。

例えば上記画像の青枠のように、赤いラインが引かれているソース部分が出てきます。

この赤い部分は、ページを読み込んだ時点で使用されていないソースなので、これらのソースを削除しても大丈夫かどうか?を確認して削除しても問題ないソースであれば削除していきます。

特に計測タグなどで使用していないものなどは削除しやすいものになります。

表示に不要なJSを遅らせる

まずは、PageSpeed Insightsの「JavaScript の実行にかかる時間の低減」に出てくるファイルに対して、読み込みを遅らせたいファイルを探します。

ここで重要なのは、初期表示に関係ないファイルが対象となります。

deferを使って初期表示に不要な部分の読み込みを遅らせる

deferは、ページの初期表示が終わってから読み込ませたいJavaScriptファイルがある場合に有効です。

方法は簡単で読み込みを遅らせたいファイルの<script src="...">のソースを探します。

ここにdeferを付けると、HTMLの解析(描画準備)をブロックせずにスクリプトをダウンロードし、DOM構築後に実行されます。

ソース例

<script src="/assets/app.js" defer></script>

結果として初期ロード中のメインスレッド占有が減り、体感速度やTBT改善につながりやすくなります。

asyncを使って実行順が不要な部分の読み込みを遅らせる

asyncは、「実行順が不要なスクリプト」だけを初期表示から切り離して遅らせる手法です。

<script src="...">にasyncを付けると、HTML解析を止めずにJSを並行で取得し、ダウンロード完了しだい即実行されます。

ソース例

<script src="https://example.com/tag.js" async></script>

そのため、計測タグ・広告・チャットなど“他のJSに依存しない外部スクリプト”の負荷を、初期描画の邪魔になりにくい形で後ろへ逃がせます。

一方で、実行順は保証されないため、ライブラリ→プラグインのように依存関係があるJSに付けると不具合の原因になります。

コード分割で初回実行を小さくする

コード分割は、JavaScriptを最初から全部配らず「初期表示に必要な最小限」だけ先に読み込み、残りは必要になった瞬間に追加で読み込む設計にすることです。

これにより、初回ロード時のJSペイロードが減り、ブラウザが行う解析・コンパイル・実行の負荷も下がって、ロングタスク原因を減らしやすくなります。

import()(遅延読み込み)で分割する

import()は「初期表示に不要な機能」を、ユーザー操作のタイミングで初めて読み込む方法です。

初期表示に不要な機能(例:チャット、地図、モーダル、重いUI部品)を“最初から読み込む”のではなく、ユーザー操作(クリック/送信)などのタイミングで import() して必要なモジュールだけ取得・実行します。

これにより初回のJSペイロードが減り、解析・コンパイル・実行の負荷(主スレッド占有)を下げやすくなります。

ソース例(クリックしたらロードさせる)

btn.addEventListener("click", async () => {
  const module = await import("./heavy-widget.js");
  module.init();
});

ロングタスクを分割する(50ms超を潰す)

1つの長いタスクを、複数の短いタスクに“時間分割”すると、タスクの合間にブラウザが入力処理・描画・レイアウトなどの高優先度処理を差し込めるようになります。

結果として「操作がすぐ返ってくる」状態になり、体感UXが改善します。

まずはDevToolsで時間のかかっているタスクを探してみましょう。

yieldでメインスレッドに一度譲る

yieldとは、長い処理の途中でいったん実行を止め、メインスレッドをブラウザ(描画・入力処理)に明け渡すことで、UIの固まりを防ぐテクニックです。

処理の区切りに scheduler.yield()(未対応ブラウザは setTimeout(0) などで代替)を挟み、残りを“次のタスク”として続行させます。

まずは、前述の通りロングタスクを探し、yieldポイントを入れる場所を決めます。

ロングタスク分割の基本は下記の順番で進めましょう。

  • ユーザーに見える処理(UI更新)を先に終わらせる(例:スピナー表示、ボタン無効化、DOM反映)
  • yield(ここで一度譲る)
  • 見えにくい重い処理(保存、集計、計測送信、巨大ループなど)を続行

手順は下記のようになります。

  1. 共通ユーティリティとして yieldToMain() を作る
  2. scheduler.yield() が使える環境ではそれを使い、無い場合は setTimeout(0) で代替

ソース例

function yieldToMain() {
  if (globalThis.scheduler?.yield) return scheduler.yield();
  return new Promise(resolve => setTimeout(resolve, 0));
}

ループ処理を“50msごと”に区切る

考え方はシンプルで、performance.now() で経過時間を測り、50msを超えそうなら一度 await yieldToMain() します。

async function runJobs(jobQueue, deadline = 50) {
  let lastYield = performance.now();

  for (const job of jobQueue) {
    job(); // ここは短く保つ

    if (performance.now() - lastYield > deadline) {
      await yieldToMain();       // ← メインスレッドに一度譲る
      lastYield = performance.now();
    }
  }
}

使い方の目安

  • deadline はまず 50ms で開始(ロングタスク基準)
  • まだ赤三角が出るなら 30〜40ms に下げる
  • 逆に yield が多すぎて全体が遅いなら 60ms まで上げてトレードオフ調整

Web Workerでメインスレッド外に逃がす

Dedicated Worker(専用ワーカー)の最小構成は、重い計算を別スレッドに移してメインスレッド(UI)を固めないための基本形です。

new Worker()でワーカーを起動し、メイン→ワーカーへはpostMessage()、ワーカー→メインへはonmessageで結果を返します。

ワーカーはDOM操作不可なので、計算・整形・圧縮などCPU処理だけ担当し、表示更新はメイン側で行います。

通信は基本「コピー」なので、データは小さく保つのがコツです。

最小構成(Dedicated Worker)の手順

  1. 切り出す処理を決める
  2. worker.jsを作る
  3. main.jsで起動
  4. データ転送のコストを抑える

まずは、パフォーマンスで50ms超や重い関数を特定し、DOM非依存の部分を選びます。

self.onmessageで受け取り→重い処理→self.postMessageで返します。

worker.jsの例

// worker.js
self.onmessage = (e) => {
  const result = heavyCompute(e.data.items);
  self.postMessage(result);
};

new Worker()してworker.postMessage()で投入、worker.onmessageで結果を受けてDOM更新します。

main.jsの例

// main.js
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.onmessage = (e) => renderResult(e.data);
worker.postMessage({ items: bigList });

大きいデータは “Transferable” でコピーコストを削る

大きいデータを Worker に渡すときは、通常の postMessage(data) だと structured clone(コピー)が走って転送に時間がかかり、結果としてメインスレッドも詰まりやすくなります。

Transferable を使うと、ArrayBufferなどの“所有権”をWorker側へ ゼロコピーで移譲でき、コピーコストを大きく削減できます。

手順

  1. 「転送したいデータ」を ArrayBuffer / TypedArray にする
  2. postMessage の第2引数(transfer list)で buffer を移譲する
  3. Worker 側では受け取った buffer から TypedArray を作り直す
  4. “コピーが必要”なケースは structuredClone の transfer オプションも検討

Transferableの基本は ArrayBufferをtransfer listに入れることです。

TypedArray 自体ではなく、裏の.bufferを渡します

// 例:大きいバイナリを作る(Uint8Array)
const u8 = new Uint8Array(10_000_000);
// ... u8 にデータを詰める

postMessage(message, [transferables...])の形で、移譲したいArrayBufferを指定します。

MDNのWorker.postMessage() にも“Transfer Example”が載っています。

// main.js(メイン → Worker)
worker.postMessage(
  { type: 'PROCESS', buffer: u8.buffer },  // データ本体
  [u8.buffer]                               // ← これが Transferable(所有権移譲)
);

// 注意:ここ以降 u8.buffer は“空”になる(byteLength=0 など)

Worker 側ではe.data.bufferを受け取り、必要な型でラップして処理します。

// worker.js
self.onmessage = (e) => {
  const { buffer } = e.data;
  const u8 = new Uint8Array(buffer); // ← 作り直す
  const result = heavyCompute(u8);

  // 返すときも大きいなら Transferable で返す
  self.postMessage({ resultBuffer: result.buffer }, [result.buffer]);
};

「元データも引き続き使いたい」場合は、所有権移譲ではなくコピーが必要です。

その場合はコピーコストが残る点は受け入れつつ、使い分けを明確にします(※ structuredClone 自体も transfer 指定が可能)。

よくある質問(Q&A)

「JavaScriptの実行にかかる時間の低減が消えないのはなぜ?」

これは“エラー”ではなく、計測環境(モバイル相当のCPUスロットル等)で JSの解析/コンパイル/実行が一定以上あると出続ける診断です。

初期表示に不要なJSや3rd party、インラインJS、拡張機能由来などが残っていると「完全に消す」より「合計時間を下げる」勝負になります。

「外部(Googleタグ等)が原因で自分で直せない場合は?」

外部タグは中身を最適化できないため、できる対策は下記になります。

  1. 不要タグの削除/整理
  2. 実行タイミングを遅らせる(同意後・操作後など)
  3. 影響度をDevToolsのRequest Blockingで“ブロック比較”して採用判断
  4. 可能なら軽い代替(サーバー側計測等)に置換

が現実的です。

「取得不可」の表記って?

Lighthouseが計測中に発生したJS実行時間のうち、どのスクリプトURLが原因か“スタック等から特定できなかった分”を指します。

つまり「犯人不明の実行時間」です。

インライン実行、計測の粒度不足、ブラウザ内部処理や外部要因などで起きます。

GitHubの説明でも「スタックが取れず、どのスクリプトにも解決できない」扱いです。

「警告2秒/Fail3.5秒はどこ基準?」

LighthouseのJavaScriptの実行にかかる時間の低減の監査のしきい値で、計測した「JSの parse/compile/evaluate 合計」が 2秒超でWarning、3.5秒超でFailになります。

Lighthouse公式ドキュメントに明記されています(診断の合格ライン)。

「JavaScriptの実行にかかる時間の低減」まとめ

「JavaScriptの実行にかかる時間の低減」は、初期表示で不要なJSを減らし、parse/compile/evaluateとロングタスク(50ms超)を潰して、TBTや体感速度を改善する取り組みです。

まずLighthouseのReduce JavaScript execution timeで重いスクリプトを特定し、defer/asyncやimport()で遅延読み込み・コード分割。

さらにyieldで50ms目安に分割し、計算系はWeb Workerへ退避します。

外部タグは整理・遅延・置換で影響を最小化。1つずつ測って確実に良くしていきましょう。

記事を書いた人

井上寛生

井上寛生

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

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