JavaScript の起動の最適化

JavaScript に大きく依存するサイトを構築すると、送信するデータに対して、必ずしも簡単に確認できるとは限らない方法でコストを支払うことがあります。この記事では、モバイル デバイスでサイトをすばやく読み込み、インタラクティブにするために、少しの規律が役立つ理由について説明します。JavaScript の配信量を減らすことで、ネットワーク送信にかかる時間を短縮し、コードの解凍にかかる時間を短縮し、この JavaScript の解析とコンパイルにかかる時間を短縮できます。

ネットワーク

ほとんどのデベロッパーは、JavaScript のコストを考えるときに、ダウンロードと実行のコストの観点から考えます。ユーザーの接続速度が遅いほど、ネットワーク経由で送信する JavaScript のバイト数が増えるため、時間がかかります。

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

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

JavaScript のネットワーク転送コストを削減するには、次の方法があります。

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

解析 / コンパイル

ダウンロード後の JavaScript の最も重いコストの一つは、JS エンジンがこのコードを解析 / コンパイルする時間です。Chrome DevTools では、解析とコンパイルは [Performance] パネルの黄色の [Scripting] 時間の一部です。

ALT_TEXT_HERE

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

ALT_TEXT_HERE
Chrome DevTools の [Performance] パネル > [Bottom-Up]。V8 のランタイム呼び出し統計情報を有効にすると、解析やコンパイルなどのフェーズに費やされた時間を確認できます。

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

ALT_TEXT_HERE

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

バイト単位で比較すると、ブラウザが JavaScript を処理するコストは、同じサイズの画像やウェブフォントを処理するコストよりも高くなります。- Tom Dale

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

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

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

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

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

ALT_TEXT_HERE
このグラフは、1 MB の JavaScript バンドル(gzip 圧縮で約 250 KB)の解析時間を、さまざまなクラスのデスクトップ デバイスとモバイル デバイスで比較したものです。解析のコストを考えるときは、解凍後の数値を考慮する必要があります。たとえば、gzip 圧縮された 250 KB の JS は、解凍すると 1 MB のコードになります。

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 Archive(上位約 50 万サイト)を使用してモバイルでの JavaScript の状態を分析すると、50% のサイトでインタラクティブになるまでに 14 秒以上かかっていることがわかります。これらのサイトでは、JS の解析とコンパイルだけで最大 4 秒かかります。

ALT_TEXT_HERE

JS やその他のリソースの取得と処理にかかる時間を考慮すると、ユーザーがページを使用できるようになるまでしばらく待たされることは、おそらく驚くことではありません。この点は、間違いなく改善の余地があります。

ページから重要でない JavaScript を削除すると、送信時間、CPU 負荷の高い解析とコンパイル、潜在的なメモリ オーバーヘッドを削減できます。これにより、ページのインタラクティブ性を高めることもできます。

実行時間

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

ALT_TEXT_HERE

スクリプトの実行時間が 50 ミリ秒を超えると、JS のダウンロード、コンパイル、実行に要する全体の時間によって、Time to Interactive が遅延します。

これに対処するため、JavaScript は小さなチャンクに分割して、メインスレッドがロックされないようにします。実行中に実行される作業量を減らすことができるかどうかを検討します。

その他の費用

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

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

JavaScript の配信コストを削減するパターン

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

PRPL

PRPL(Push、Render、Pre-cache、Lazy-load)は、積極的なコード分割とキャッシュ保存によってインタラクティビティを最適化するパターンです。

ALT_TEXT_HERE

その影響を可視化してみましょう。

V8 のランタイム呼び出し統計情報を使用して、一般的なモバイルサイトとプログレッシブ ウェブアプリの読み込み時間を分析します。ご覧のとおり、解析時間(オレンジ色で表示)は、これらのサイトの多くが時間を費やしている部分の大部分を占めています。

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 のコストを監視できるようにします。

詳細