JavaScript 起動の最適化

Addy Osmani
Addy Osmani

JavaScript への依存度が高いサイトを作るようになると、送信したデータに対して料金を支払っても、簡単には確認できないことがあります。この記事では、モバイル デバイスでサイトをすばやく読み込んでインタラクティブにするために、ちょっとした規律が役立つ理由について説明します。配信する JavaScript の量を減らすと、ネットワーク伝送時間が短縮され、コードの解凍時間が短縮され、この JavaScript の解析とコンパイルにかかる時間が短くなります。

ネットワーク

ほとんどのデベロッパーが JavaScript のコストについて考えるとき、ダウンロードと実行のコストについて考えてみます。転送する JavaScript のバイト数が増えると、ユーザーの接続速度が遅くなります。

ブラウザがリソースをリクエストしたら、そのリソースを取得して解凍する必要があります。JavaScript のようなリソースの場合は、実行前に解析してコンパイルする必要があります。

ユーザーが使用している有効なネットワーク接続タイプが実際には 3G、4G、Wi-Fi ではない可能性があるため、これは問題になることがあります。コーヒーショップの Wi-Fi を利用しても、2G の速度のモバイル アクセス ポイントに接続している場合があります。

JavaScript のネットワーク転送コストは、次の方法で削減できます。

  • ユーザーが必要とするコードのみを送信する
    • コード分割を使用して、JavaScript を重要なものとそうでないものに分割します。webpack などのモジュール バンドラはコード分割をサポートしています。
    • 重要でないコードの遅延読み込み。
  • 圧縮
  • 圧縮
    • テキストベースのリソースを圧縮するには、少なくとも gzip を使用してください。
    • Brotli ~ q11 の使用を検討してください。Brotli は圧縮率の点で gzip より優れています。これにより、CertSimple は圧縮 JS バイトのサイズを 17%、LinkedIn は読み込み時間を 4% 削減できました。
  • 未使用のコードを削除する
  • コードをキャッシュに保存してネットワーク トリップを最小限に抑える。
    • HTTP キャッシュを使用して、ブラウザがレスポンスを効果的にキャッシュに保存できるようにします。スクリプトの最適な存続期間(max-age)を決定し、検証トークン(ETag)を提供して、変更されていないバイトが転送されないようにします。
    • Service Worker のキャッシュ保存により、アプリのネットワークの復元性が向上し、V8 のコード キャッシュなどの機能に積極的にアクセスできるようになります。
    • 変更されていないリソースを再取得する必要がないように、長期キャッシュを使用します。Webpack を使用している場合は、ファイル名のハッシュをご覧ください。

解析/コンパイル

ダウンロード後、JavaScript で最もコストがかかるのは、JS エンジンがこのコードを解析/コンパイルする時間です。Chrome DevTools では、解析とコンパイルは [パフォーマンス] パネルの黄色の [スクリプト] 時間の一部です。

ALT_TEXT_HERE

[Bottom-Up] タブと [Call Tree] タブには、解析とコンパイルの正確なタイミングが表示されます。

ALT_TEXT_HERE
Chrome DevTools の [パフォーマンス] パネル > [ボトムアップ]V8 の Runtime Call Stats を有効にすると、解析やコンパイルなどのフェーズで費やされた時間を確認できます。

なぜこれが重要なのでしょうか

ALT_TEXT_HERE

コードの解析やコンパイルに長い時間を費やすと、ユーザーがサイトを操作するまでの時間が大幅に長くなる可能性があります。送信する JavaScript が多いほど、サイトがインタラクティブになるまでの解析とコンパイルにかかる時間が長くなります。

バイト単位では、JavaScript は同等のサイズの画像やウェブフォントよりもブラウザでの処理コストが高い - Tom Dale

JavaScript と比較して、同等サイズの画像の処理には多くの費用がかかります(デコードが必要です)。平均的なモバイル ハードウェアでは、JS はページのインタラクティビティに悪影響を与える可能性が高くなります。

ALT_TEXT_HERE
JavaScript と画像のバイトではコストが大きく異なります。画像は通常、デコードとラスタライズ中にメインスレッドをブロックしたり、インターフェースがインタラクティブになったりすることはありません。ただし、JS は解析、コンパイル、実行のコストのため、インタラクティビティが遅延する可能性があります。

解析とコンパイルが遅いことについて話すときは、コンテキストが重要です。ここでは平均的なスマートフォンについて説明しています。平均的なユーザーは、低速の CPU と GPU を搭載し、L2/L3 キャッシュがなく、メモリに制約があるスマートフォンも使用している可能性があります。

ネットワーク機能とデバイスの機能は必ずしも一致しません。優れた Fiber 接続を利用しているユーザーが、デバイスに送信された JavaScript を解析し、評価するのに必ずしも最適な CPU を持っているとは限りません。これは逆に、ネットワーク接続はひどいが、CPU は非常に高速です。- Kristofer Baxter 氏 LinkedIn

ローエンドおよびハイエンドのハードウェアで解凍した(シンプルな)JavaScript を約 1 MB 解析する場合のコストを以下に示します。市場で最も高速なスマートフォンと平均的なスマートフォンでは、コードの解析/コンパイルにかかる時間が 2 ~ 5 倍あります

ALT_TEXT_HERE
このグラフは、クラスが異なるデスクトップ デバイスとモバイル デバイスにまたがる 1 MB の JavaScript バンドル(gzip で 250 KB 以下)の解析時間を示しています。

CNN.com のような実際のサイトの場合、

ハイエンドの iPhone 8 では、CNN の JS の解析とコンパイルにわずか 4 秒しかかかりませんが、平均的なスマートフォン(Moto G4)では約 13 秒しかかかりません。これは、ユーザーがこのサイトを完全に操作するまでの時間に大きな影響を与える可能性があります。

ALT_TEXT_HERE
上記は、Apple の A11 Bionic チップと、平均的な Android ハードウェアの Snapdragon 617 のパフォーマンスを比較した解析時間を示しています。

このことから、ポケットに収まるスマートフォンだけではなく、平均的なハードウェア(Moto G4 など)でテストすることが重要であることがわかります。ただし、コンテキストは重要です。ユーザーのデバイスやネットワークの状態に合わせて最適化できます。

ALT_TEXT_HERE
Google アナリティクスでは、実際のユーザーがサイトへのアクセスに使用しているモバイル デバイス クラスに関する分析情報を得ることができます。これにより、動作中の実際の CPU/GPU の制約を把握できます。

JavaScript を送りすぎていないかその可能性はあります。

HTTP アーカイブ(上位 50 万以上のサイト)を使用してモバイルの JavaScript の状態を分析すると、50% のサイトが操作可能になるのに 14 秒以上かかっていることがわかります。これらのサイトでは、JS の解析とコンパイルに最大 4 秒かかります。

ALT_TEXT_HERE

JS やその他のリソースを取得して処理するのに要する時間を考慮してください。ページが使用可能になったと感じるまでユーザーがしばらく待たされることは、驚くことではないでしょう。改善の余地は十分にあるはずです。

重要性の低い JavaScript をページから削除すると、転送時間、CPU 使用率の高い解析とコンパイル、メモリのオーバーヘッドを削減できます。また、ページをインタラクティブにすばやく開くこともできます。

実行時間

解析とコンパイルだけではなく、コストもかかります。JavaScript の実行(解析/コンパイル後のコードの実行)は、メインスレッドで実行する必要があるオペレーションの 1 つです。実行時間が長くなると、ユーザーがサイトを操作できるまでの時間も長くなる可能性があります。

ALT_TEXT_HERE

スクリプトが 50 ミリ秒を超えて実行されると、JS のダウンロード、コンパイル、実行に要する時間全体の分だけインタラクティブまでの時間が長くなります(Alex Russell)

これに対処するために、JavaScript では、メインスレッドのロックアップを回避するために、小さなチャンクを使用するという利点があります。実行中の作業量を減らすことができるかどうかを調べる。

その他の費用

JavaScript はページのパフォーマンスに次のような影響を与える可能性があります。

  • メモリ。GC(ガベージ コレクション)が原因で、ページが頻繁にジャンクしたり一時停止になったりすることがあります。ブラウザがメモリを再利用すると、JS の実行が一時停止するため、ガベージ コレクションを頻繁に行うブラウザは、実行を一時停止する頻度が想定より高くなる可能性があります。メモリリークや gc の頻繁な一時停止を回避して、ページでジャンクが発生しないようにしてください。
  • ランタイムに長時間実行される JavaScript によってメインスレッドがブロックされ、ページが応答しなくなることがあります。作業を小さく分割(スケジュール設定に requestAnimationFrame() または requestIdleCallback() を使用)すると、応答性の問題を最小限に抑えることができ、Interaction to Next Paint(INP)を改善できます。

JavaScript 配信費用を削減するためのパターン

JavaScript の解析/コンパイル時間とネットワーク送信時間を遅くしたい場合、ルートベースのチャンクや PRPL などのパターンが役立ちます。

PRPL

PRPL(プッシュ、レンダリング、事前キャッシュ、遅延読み込み)は、積極的なコード分割とキャッシュによってインタラクティビティを最適化するパターンです。

ALT_TEXT_HERE

その効果を可視化してみましょう。

Google では V8 の Runtime Call Stats を使用して、人気の高いモバイルサイトとプログレッシブ ウェブアプリの読み込み時間を分析しています。ご覧のとおり、解析時間(オレンジ色で表示)は、これらのサイトの多くが時間を費やしている部分の大部分を占めています。

ALT_TEXT_HERE

PRPL を使用するサイトの Wego は、ルートの解析時間を短く保ち、非常に迅速にインタラクティブになっています。上記の他のサイトの多くは、JS 費用を削減するために、コード分割とパフォーマンス バジェットを採用しました。

プログレッシブ ブートストラップ

多くのサイトでは、インタラクティビティを犠牲にして、コンテンツの視認性を最適化しています。サイズの大きい JavaScript バンドルがある場合にファースト ペイントを高速化するために、デベロッパーはサーバーサイド レンダリングを採用することがあります。これは、JavaScript が最終的に取得されたときにイベント ハンドラをアタッチするように「アップグレード」します。

これには費用がかかりますので、ご注意ください。通常は、1)大きな HTML レスポンスを送信するため、インタラクティビティが促進されます。2)JavaScript の処理が完了するまで、エクスペリエンスの半分が実際にはインタラクティブにならない不気味な谷にユーザーが留まる可能性があります。

プログレッシブ ブートストラップのほうが良いアプローチかもしれません。最小限の機能のページ(現在のルートに必要な HTML/JS/CSS のみで構成)を送ります。より多くのリソースが到着すると、アプリはより多くの機能を遅延読み込みしてロック解除します。

ALT_TEXT_HERE
Progressive Bootstrapping(Paul Lewis)

表示内容に合わせてコードを読み込むことが、究極の目標です。PRPL とプログレッシブ ブートストラップは、これを行うために役立つパターンです。

まとめ

送信サイズはローエンド ネットワークにとって重要です。解析時間は CPU バウンドのデバイスにとって重要です。これを低く抑えることが重要になります。

チームは、JavaScript の転送時間と解析/コンパイル時間を短く抑えるために、厳格なパフォーマンス バジェットの導入に成功しています。Alex Russell の「Can You Afford It?: Real World Web Performance Budgets」をご覧ください。

ALT_TEXT_HERE
アーキテクチャに関する決定によってアプリのロジックにどの程度の JS の「ヘッドルーム」が生じるかを考慮することは有益です。

モバイル デバイスをターゲットとするサイトを構築する場合は、代表的なハードウェアで開発するように最善を尽くし、JavaScript の解析/コンパイル時間を短く抑え、チームが JavaScript の費用を把握できるようにパフォーマンス バジェットを採用します。

詳細