【表示速度の改善検証】重いjavascriptを最適化したらどのくらい変わる?

【表示速度の改善検証】重いjavascriptを最適化したらどのくらい変わる?

重いjavascriptを最適化したらどのくらい変わる?

今回はこのテーマで検証してみたいと思います。

重いjavascriptを表示速度改善するための検証条件

5Mのjavascriptを実装しているページを用意します。

これをさまざまな方法でどう最適化がかけられるのか?どの程度改善できるかを検証していきます。

実際のスタート時の状況です。

PageSpeed Insightsの初期数値

特にレンダリングブロックで影響の出るLCPは26.7秒でした。

「レンダリングをブロックしているリクエスト」の項目では、推定される削減時間が26,340ミリ秒となっています。

DevToolsで実際に読み込んでみると下記のようになりました。

見てわかるように、重い要因となっている「dummy1.js」のファイルが読み込まれるまでページが表示されないことがわかります。

「dummy1.js」のファイルが全て読み込み終わってから、ページの内容が表示されています。

ウォーターフォールを見てもわかるように、DOMcontentLoaded(下記画像の青いラインでbodyが読み込み開始するまでにかかる時間)が31.7秒ほどかかっています。

もちろん、この読み込みが行われている間はページには何も表示されません。

レンダリングブロックされている間はページに何も表示されない時間が続いていることがわかります。

改善方法①:不要なコード削除した場合

例えば、このファイルがこのページを表示させるのに不要な場合のケースです。

他のページでは使用されているけど、このページでは使われていないなどのケースが該当します。

下記の画像が修正前のHTMLとなります。

重い原因となっている下記のソースを1行消します。

<script src="dummy1.js"></script>

消したあとのHTMLが下記となります。

このファイルをアップし直して、PageSpeed InsightsとDevToolsを確認してみましょう。

PageSpeed Insightsでは下記のように変化しました。

LCPの秒数が26.7秒から3.5秒

それと並行して、FCPの数値やSpeed Indexの数値も上がりました。

また、「レンダリングをブロックしているリクエスト」の項目でも、推定される削減時間が3,290ミリ秒まで減りました。

DevToolsでも読み込み直してみると下記のようになりました。

まず「dummy1.js」のファイルは削除されているのがわかると思います。

ページでコンテンツが表示されるまでの時間も相当速くなりましたし、DOMcontentLoaded(下記画像の青いラインでbodyが読み込み開始するまでにかかる時間)が1.45秒ほどとかなり速くなっていることがわかります。

完全に不要なjavascriptであれば、削除することで表示速度にかなりインパクトを与えることがわかると思います。

改善方法②:javascriptを後から読み込ませた場合

例えば、ファーストビューの表示にこのjavascriptが直接関与していない場合などにこの方法は有効です。

ページ内のコンテンツに関与しているけど、ページの下部にあるコンテンツで後から読み込んでも大丈夫な場合などです。

方法は2つあって、deferを使う方法か、asyncを使う方法があります。

それぞれHTMLの解析を止めずに読み込みができる分、表示までの速度が速くなります。

deferを使ってjavascriptの読み込みを遅らせる

HTML解析を止めずにJSを並行で取得するという意味では、deferもasyncも同じです。

deferの場合は、DOM構築後にダウンロードが開始されるのが特徴です。

今回の検証では、重い「dummy1.js」のファイルに対してdeferを設置します。

上記の画像のようにHTMLにdeferを追加するだけで対応できます。

ソースは下記のように変更を加えています。

<script src="dummy1.js" defer></script>

PageSpeed Insightsでは下記のように変化しました。

LCPはソースを削除した時と同様に3.5秒となりました。

deferでjavascriptの読み込みを遅らせた場合、LCPに関してはソースを削除した時と同じインパクトがあります。

ただspeed indexの秒数も下がったものの、削除時ほどパフォーマンスは上がりませんでした。

「レンダリングをブロックしているリクエスト」の項目では、「dummy1.js」のファイルは削除時と同様に対象外となりました。

要はレンダリングブロックに認定されなくなったということになります。

この動画を見てわかるようにdeferを入れると、ページ自体の表示も邪魔されず表示されていることがわかります。

ウォーターフォールを見ても、「dummy1.js」のファイルがずっと読み込み中でも他のファイルの読み込みを邪魔していないことがわかります。

asyncを使ってjavascriptの読み込みを遅らせる

asyncは外部JSをHTML解析と並行で取得し、取得完了次第すぐ実行します。

deferと違い実行順は保証されず、実行時にHTML解析が一時停止することがあります。

独立した計測タグ等に向き、DOM依存や順序依存スクリプトには不向きです。

上記の画像のようにHTMLにasyncを追加するだけで対応できます。

ソースは下記のように変更を加えています。

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

PageSpeed Insightsでは下記のように変化しました。

FCPの数値は3.5秒とdeferの時と同様でしたが、LCPは29.1秒とdefer時よりも遅い結果になりました。

asyncはダウンロード完了時に即実行されるため、HTML解析中でも処理が割り込まれます。

その結果、メインスレッドが一時停止し、LCP要素の描画が遅れる可能性があります。

deferはDOM構築後に順番実行されるため描画を妨げにくく、LCPが安定しやすいのに対し、asyncは実行タイミングが不定でLCPが悪化しやすくなります。

「レンダリングをブロックしているリクエスト」の項目では、「dummy1.js」のファイルは削除時と同様に対象外となりました。

要は読み込みを遅らせて、レンダリングブロックは回避できているが、LCPの数値は実行タイミングの影響で数値が悪化しやすくなっていることがわかります。

目的や用途によってasyncを使うことはあると思いますが、LCPの数値を意識していく場合はdeferを活用していく方が数値がよくなるという結論になります。

改善方法③:コード分割で初回表示の実行を最小限にする

コード分割は、初回に必要な最小コードだけを読み込み、残りは画面遷移・クリック時にimport()で遅延読込する手法。

初回JS実行量を減らし、描画開始を早める。ルート/機能単位で分け、重い処理や管理画面は後回しにする。

ローディング表示と失敗時の再試行も用意し、体感速度を維持する。

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

まずは import() を使わず、代わりに最上部で 静的 import をしているファイルを用意しました。

実際に見てもらうと、ページを読み込み〜表示された時点で「heavy-module.js」のファイルが読み込まれているのがわかります。

今回の import() での分割では、下記のソースを挿入してボタンを押す操作をするタイミングで「heavy-module.js」を読み込むという形にします。

 const mod = await import('./heavy-module.js');

下記がimport() で分割しているファイルを活用した場合の動画です。

実際にページ読み込んで表示されるまでの間は「heavy-module.js」のファイルは読み込まれていません。

ボタンをクリックした操作を行なった後に「heavy-module.js」が読み込まれるのがわかります。

初期表示では「heavy-module.js」の分の読み込みがないため、その分の読み込み速度の改善やTBT数値が改善されます。

今回はボタンでの例でしたが、スクロールでの表示、ホバー、アコーディオン、タブ切り替えなどで活用できます。

改善方法④:ロングタスクを分割する(50ms超を潰す)

ロングタスク分割とは、50ms超メインスレッドを占有する重い処理を、数ms〜十数msの小さなチャンクに切り、各チャンク実行後にrequestAnimationFrameやsetTimeoutでブラウザへ制御を返す手法です。

描画・入力・クリック処理が割り込めるためUIフリーズを防ぎ、Long Taskを減らして体感性能を改善します。

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

yieldでメインスレッドに一度譲る仕組みとして、重い処理を小さなチャンクに分割し、各チャンクの終了ごとに requestAnimationFrame や setTimeout(0)、await scheduler.yield() などで制御をイベントループへ戻す手法です。

描画・入力・クリック処理が割り込めるため、50ms超の連続ブロック(Long Task)を減らし、表示更新が途中から見えてUIフリーズを防げます。

yieldを使用せずにjavascriptを動かすと下記のようになります。

結果として50ms超のロングタスクが発生するページとなっています。

これに対し、ブラウザに描画・入力処理の時間を渡すために requestAnimationFrame で待つ関数を作って検証してみます。

下記がyieldの部分のソース例になります。

// requestAnimationFrame まで待つ = 次の描画タイミングに譲る(yield)
const yieldToBrowser = () => new Promise(requestAnimationFrame);

実際に計測を行なってみると下記のようになりました。

ビフォーはロングタスクが残ったのに対し、yieldを使ったアフターの場合は分割されロングタスクの発生がなくなりました。

改善方法⑤:Web Workerでメインスレッドから外す

Web Workerは、JavaScriptの重い処理をメインスレッド(UIスレッド)から分離して実行するための仕組みです。

バックグラウンドで別スレッドを作成し、そこで計算処理やデータ処理を実行することで、ブラウザのUIをブロックせずにスムーズな操作性を維持できます。

postMessage()でメインスレッドとWorker間でデータをやり取りし、重い処理によるフリーズを防ぎ、ユーザー体験を向上させます。

今回は検証のために下記のようなページを作成しました。

「Before: メインスレッド」では、実行ボタンを押すとmain.jsというファイルで計算を行うような仕組みになっています。

「After: Web Worker」では実行ボタンを押すとmain.jsというファイルがworker.jsというファイルを実行させ、Web Workerを動かすような仕組みになっています。

main.jsのファイルには下記のソースを入れてWorker生成(= 別スレッドを作る)を行ない、worker.jsが別スレッドとして起動されます。

worker = new Worker('worker.js');

下記の動画は「Before: メインスレッド」を実際に動かした動画になります。

「Before: メインスレッド」では下記のようにメインスレッドでロングタスクが発生していることがわかります。

それぞれ処理時間も長いことがわかります。

逆にWeb Workerを使用している場合は、下記のようになります。

先ほどのDevToolsのパフォーマンスにあるworkerというタブを開くと、worker.jsでの処理の様子がわかります。

メインスレッドの方では、ロングタスクは発生していないことがわかります。

こうすることで、メインスレッドを邪魔せずに処理が行われていることがわかります。

重いjavascriptを最適化まとめ

重いJavaScriptは、まず計測して原因を特定し、長いタスクを分割・遅延し、不要な処理と再描画を減らすのが近道です。

Web Workerで計算を逃がし、postMessageの転送量も最小化。

コード分割やキャッシュで初期負荷を抑え、INPやLong Taskを指標に改善を回しましょう。

小さな一歩でも確実に効きます。

最終的に“体感が良い”をゴールに、継続的にも磨きましょう!

記事を書いた人

井上寛生

井上寛生

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

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