ホームページ > 記事 > ウェブフロントエンド > JavaScript 起動パフォーマンスのボトルネック分析と解決策
Web 開発では、需要が増加し、コードベースが拡大するにつれて、最終的に公開する Web ページも徐々に拡大します。ただし、この拡張は、より多くの伝送帯域幅を占有することを意味するだけでなく、ユーザーが Web を閲覧するときにパフォーマンスが低下する可能性があることも意味します。ブラウザーは、特定のページが依存するスクリプトをダウンロードした後も、構文分析、解釈、実行の次の手順を実行する必要があります。この記事では、ブラウザの JavaScript 処理を詳細に分析し、アプリケーションの起動時間に影響を与える原因を特定し、私の個人的な経験に基づいて対応する解決策を提案します。振り返ってみると、JavaScript の解析/コンパイル手順を最適化する方法については特に考えていませんでした。パーサーが 3f1c4e4b6b16bbbd69b2ee476dc4f83a
タグを検出した後、即座に解析操作を完了すると予想していましたが、これは明らかに夢物語です。次の図は、V8 エンジンの動作原理の概要です:
主要な手順を詳しく分析してみましょう。
起動フェーズでは、JavaScript エンジンの実行時間のほとんどが構文分析、コンパイル、スクリプトの実行に費やされます。言い換えれば、これらのプロセスによって生じる遅延は、ユーザーの操作の遅延を正確に反映します。たとえば、ユーザーがボタンを見ても、実際にクリックできるようになるまでに数秒かかり、ユーザー エクスペリエンスに大きな影響を与えます。
上の写真は、Chrome Canary に組み込まれた V8 ランタイム呼び出し統計を使用した特定の Web サイトの分析結果です。デスクトップ ブラウザーでは構文解析とコンパイルに依然として時間がかかり、モバイルでは構文解析とコンパイルに時間がかかることに注意してください。ターミナルの方が時間がかかります。実際、Facebook、Wikipedia、Reddit などの大規模な Web サイトで構文解析とコンパイルに費やされる時間は無視できません:
上の図のピンク色の領域は V8 と Blink の C++ で費やされる時間を表し、オレンジと黄色は、それぞれ構文解析とコンパイルの時間の割合を示します。 Facebook の Sebastian Markbage と Google の Rob Wormald も、JavaScript の長い構文解析時間が無視できない問題になっていると Twitter に投稿しました。後者も、これが Angular を開始する際の主な消費の 1 つであると述べています。
モバイル端末の急増に伴い、私たちは残酷な事実に直面しなければなりません。モバイル端末での同じパッケージ本体の解析とコンパイルのプロセスには、デスクトップブラウザの 2 ~ 5 倍の時間がかかります。もちろん、iPhone や Pixel などのハイエンドの携帯電話は、Moto G4 のようなミッドレンジの携帯電話よりもはるかに優れたパフォーマンスを発揮します。これは、テストするときに、周囲のハイエンドの携帯電話だけを使用するのではなく、両方のミッドレンジの携帯電話を考慮する必要があることを思い出させます。 -範囲およびローエンドの携帯電話:
上の図は、一部のデスクトップ ブラウザとモバイル ブラウザの間で 1 MB の JavaScript パッケージ本体の解析時間を比較したものです。さまざまな構成。アプリケーション パッケージ本体がすでに非常に大きい場合、コード分割、TreeShaking、Service Workder キャッシュなどの最新のパッケージ化手法を使用すると、起動時間に大きな影響を与えます。別の観点から見ると、たとえそれが小さなモジュールであっても、コードの書き方が不十分であったり、依存関係ライブラリの使用が不十分であったりすると、メインスレッドはコンパイルや冗長な関数呼び出しに多くの時間を費やすことになります。実際のパフォーマンスのボトルネックを掘り出すには、総合的な評価の重要性を明確に理解する必要があります。
私は Facebook ではありません、という言葉を何度も聞きました。あなたが言及した JavaScript の構文解析とコンパイルは、他の Web サイトにどのような影響を及ぼしますか?私もこの問題に興味があったので、2 か月かけて 6,000 を超える Web サイトを分析しました。これらの Web サイトには、React、Angular、Ember、Vue などの人気のあるフレームワークやライブラリが含まれていました。ほとんどのテストは WebPageTest に基づいているため、これらのテスト結果を簡単に再現できます。
光ファイバーアクセスを備えたデスクトップブラウザではユーザー操作が可能になるまでに約 8 秒かかりますが、3G 環境の Moto G4 ではユーザー操作が可能になるまでに約 16 秒かかります。
ほとんどのアプリケーションは、JavaScript の起動フェーズ (文法解析、コンパイル、実行) に約 4 秒かかります デスクトップ ブラウザでは:
、モバイル ブラウザでは約 36 秒かかります構文を解析する時間の割合: さらに、統計によれば、すべての Web サイトがユーザーに巨大な JS パッケージ本体をスローしているわけではありません。ユーザーがダウンロードした Gzip 圧縮されたパッケージの平均サイズは 410KB であり、これは基本的にHTTPArchive によって以前にリリースされた 420 KB のデータ。しかし、最悪の Web サイトは 10 MB のスクリプトをユーザーに直接ダンプするもので、これは単純にひどいものです。 上記の統計から、パッケージ本体のボリュームは重要ですが、それだけが要因ではないことがわかります。構文解析とコンパイルにかかる時間は、パッケージ本体の増加に応じて必ずしも直線的に増加するとは限りません。音量。一般に、小さい JavaScript パッケージは (ブラウザー、デバイス、ネットワーク接続の違いを無視して) 読み込みが速くなりますが、同じサイズの 200KB では、さまざまな開発者のパッケージの構文解析とコンパイル時間は膨大になります。お互いにあり、比較することはできません。 最新の JavaScript 構文解析とコンパイルのパフォーマンス評価Chrome DevToolsタイムライン (パフォーマンス パネル) を開く > ボトムアップ/コール ツリー/イベント ログには、現在の Web サイトの構文解析/コンパイルに費やされた時間の割合が表示されます。より完全な情報が必要な場合は、V8 のランタイム コール統計をオンにできます。 Canary では、[Experims] > [V8 Runtime Call Stats] のタイムラインにあります。 Chrome トレース Chrome が提供する基礎的な追跡ツールを使用すると、disabled-by-default-v8.runtime_stats
を使用して詳細を理解できます。 V8 の消費時間。 V8 では、この機能の使用方法に関する詳細なガイダンスも提供されます。 disabled-by-default-v8.runtime_stats
来深度了解 V8 的时间消耗情况。V8 也提供了详细的指南来介绍如何使用这个功能。
WebPageTest 中 Processing Breakdown 页面在我们启用 Chrome > Capture Dev Tools Timeline 时会自动记录 V8 编译、EvaluateScript 以及 FunctionCall 的时间。我们同样可以通过指明disabled-by-default-v8.runtime_stats
WebPageTest
WebPageTest の処理内訳ページChrome > Capture Dev Tools Timeline が有効になっている場合、V8 のコンパイル時間、EvaluateScript 時間、および FunctionCall 時間が自動的に記録されます。disabled-by-default-v8.runtime_stats
を指定して、ランタイム呼び出し統計を有効にすることもできます。 🎜🎜🎜🎜🎜詳しい手順については、私の要点を参照してください。 🎜Nolan Lawson が推奨する User Timing API を使用して、文法解析の時間を評価することもできます。ただし、このメソッドは V8 の事前解析プロセスの影響を受ける可能性があります。optimize-js の評価で Nolan のメソッドから学び、スクリプトの最後にランダムな文字列を追加することで、この問題を解決できます。実際のユーザーやデバイスがウェブサイトにアクセスしたときの解析時間を評価するために、Google Analytics に基づく同様の方法を使用しています:
Etsy の DeviceTiming ツールは、特定の制限された環境をシミュレートして、ページの構文解析と実行時間を評価できます。 。ローカル スクリプトをインストルメンテーション ツール コードでラップして、ページがさまざまなデバイスからのアクセスをシミュレートできるようにします。詳しい使用法については、Daniel Espeset の記事「モバイル デバイスでの JS 解析と実行のベンチマーク」を参照してください。
JavaScript パッケージ本体のサイズを削減します。また、パッケージ本体が小さいほど解析ワークロードが少なくなるため、解析およびコンパイルのフェーズでのブラウザの時間消費も削減できることも上で説明しました。
コード分割ツールを使用してオンデマンドでコードを渡し、残りのモジュールを遅延ロードします。 PRPL のようなモデルはルートベースのグループ化を促進し、現在 Flipkart、Housing.com、Twitter で広く使用されているため、これがおそらく最良のアプローチです。
スクリプト ストリーミング: 以前、V8 は開発者に async/defer
を使用して、スクリプト ストリーミングに基づいて 10 ~ 20% のパフォーマンス向上を達成することを推奨していました。この技術により、HTML パーサーは対応するスクリプト読み込みタスクを専用のスクリプト ストリーミング スレッドに割り当てることができるため、ドキュメント解析のブロックが回避されます。 V8 では、ストリーマ スレッドが 1 つしかないため、できるだけ早く大きなモジュールをロードすることをお勧めします。
依存関係の解析コストを評価します。 React の代わりに Preact または Inferno を使用するなど、機能は同じで読み込みが速い依存関係を選択するよう最善を尽くす必要があります。これらは、React よりもサイズが小さく、必要な構文解析とコンパイル時間が短くなります。 Paul Lewis も最近の記事でフレームワークの起動コストについて議論しており、これは Sebastian Markbage の声明と一致しています。フレームワークの起動コストを評価する最良の方法は、最初にインターフェイスをレンダリングし、次に削除し、最後に再レンダリングすることです。最初のレンダリング プロセスには分析とコンパイルが含まれます。比較を通じて、フレームワークの起動時の消費量を確認できます。
JavaScript フレームワークが AOT (ahead-of-time) コンパイル モードをサポートしている場合、解析とコンパイルの時間を効果的に短縮できます。 Angular アプリケーションはこのパターンの恩恵を受けます:
落胆しないでください。起動時間を改善する方法に苦労しているのはあなただけではありません。私たちの V8 チームも懸命に取り組んでいます。以前の評価ツールである Octane は、実際のシナリオの優れたシミュレーションであることがわかりました。マイクロ フレームワークとコールド スタートの点で、実際のユーザーの習慣と非常に一致しています。これらのツールに基づいて、V8 チームは過去の作業で起動パフォーマンスの約 25% の向上も達成しました:
このセクションでは、過去数年間に構文解析とコンパイル時間を改善するために使用したテクニックをレビューします。 。 精巧な。
Chrome 42 では、いわゆるコード キャッシュの概念が導入され始めました。これにより、ユーザーがアクセスしたときにスクリプトのクロールを回避できるように、コンパイルされたコードのコピーを保存するメカニズムが提供されます。もう一度ページを開き、これらの手順を解析してコンパイルします。さらに、このメカニズムにより、繰り返しアクセス中のコンパイル時間の約 40% も回避できることもわかりました。ここでいくつかの内容を詳しく紹介します。
コード キャッシュは、72 時間以内に繰り返し実行されるコードをキャッシュします。スクリプトは機能します。
Service Worker のスクリプトの場合、コード キャッシュは 72 時間以内のスクリプトに対しても機能します。
Service Worker を使用してキャッシュ ストレージにキャッシュされたスクリプトの場合、スクリプトが最初に実行されるときにコード キャッシュが有効になることがあります。
つまり、アクティブにキャッシュされた JavaScript コードの場合、最大でも 3 回目の呼び出しで構文分析とコンパイルの手順をスキップできます。 chrome://flags/#v8-cache-strategies-for-cache-storage
を通じて違いを確認するか、js-flags=profile-deserialization
を設定して Chrome を実行しますコードがコード キャッシュからロードされているかどうかを確認します。ただし、コード キャッシュ メカニズムはコンパイル済みコードのみをキャッシュすることに注意してください。これは主に、グローバル変数の設定によく使用されるトップレベル コードを指します。関数定義などの遅延コンパイルされたコードはキャッシュされませんが、IIFE は V8 にも含まれているため、これらの関数もキャッシュできます。 chrome://flags/#v8-cache-strategies-for-cache-storage
来查看其中的差异,也可以设置 js-flags=profile-deserialization
运行 Chrome 来查看代码是否加载自代码缓存。不过需要注意的是,代码缓存机制仅会缓存那些经过编译的代码,主要是指那些顶层的往往用于设置全局变量的代码。而对于类似于函数定义这样懒编译的代码并不会被缓存,不过 IIFE 同样被包含在了 V8 中,因此这些函数也是可以被缓存的。
Script Streaming允许在后台线程中对异步脚本执行解析操作,可以对于页面加载时间有大概 10% 的提升。上文也提到过,这个机制同样会对同步脚本起作用。
这个特性倒是第一次提及,因此 V8 会允许所有的脚本,即使阻塞型的d1074361c4bc125817d0106ef5f7b412
脚本也可以由后台线程进行解析。不过缺陷就是目前仅有一个 streaming 后台线程存在,因此我们建议首先解析大的、关键性的脚本。在实践中,我们建议将b6abb9f95e1e7d03e71e2a9f71e7c034
添加到93f0f5c25f18dab9d176bd4f6de5d30e
块内,这样浏览器引擎就能够尽早地发现需要解析的脚本,然后将其分配给后台线程进行处理。我们也可以查看 DevTools Timeline 来确定脚本是否被后台解析,特别是当你存在某个关键性脚本需要解析的时候,更需要确定该脚本是由 streaming 线程解析的。
我们同样致力于打造更轻量级、更快的解析器,目前 V8 主线程中最大的瓶颈在于所谓的非线性解析消耗。譬如我们有如下的代码片:
(function (global, module) { … })(this, function module() { my functions })
V8 并不知道我们编译主脚本的时候是否需要module
这个模块,因此我们会暂时放弃编译它。而当我们打算编译module
时,我们需要重分析所有的内部函数。这也就是所谓的 V8 解析时间非线性的原因,任何一个处于 N 层深度的函数都有可能被重新分析 N 次。V8 已经能够在首次编译的时候搜集所有内部函数的信息,因此在未来的编译过程中 V8 会忽略所有的内部函数。对于上面这种module
635ab0ca10821c7db2859c98ad4ae1aa
スクリプトはバックグラウンド スレッドによって解析することもできます。ただし、現時点ではストリーミング バックグラウンド スレッドが 1 つしかないという欠点があるため、最初に大規模で重要なスクリプトを解析することをお勧めします。実際には、ブラウザ エンジンが解析する必要があるスクリプトをできるだけ早く検出できるように、b6abb9f95e1e7d03e71e2a9f71e7c034
を 93f0f5c25f18dab9d176bd4f6de5d30e
ブロック内に追加することをお勧めします。次に、それを処理のためにバックグラウンド スレッドに割り当てます。また、DevTools タイムラインをチェックして、スクリプトがバックグラウンドで解析されているかどうかを確認することもできます。特に、解析する必要がある重要なスクリプトがある場合は、そのスクリプトがストリーミング スレッドによって解析されていることを確認する必要があります。 A 文法解析とコンパイルの最適化私たちは、現在 V8 メインスレッドで最大の、より軽量で高速なパーサーの構築にも取り組んでいます。いわゆる非線形解析の消費。たとえば、次のコード部分があります: rrreee
V8 メインスクリプトをコンパイルするときにmodule
モジュールが必要かどうかわからないため、一時的にコンパイルを諦めます。 module
をコンパイルする場合は、すべての内部関数を再分析する必要があります。これが、V8 解析時間のいわゆる非線形性の理由です。N レベルの深さの関数は N 回再解析される可能性があります。 V8 は、最初のコンパイル時にすべての内部関数に関する情報を収集できるため、今後のコンパイルではすべての内部関数を無視します。 module
形式の上記の関数については、パフォーマンスが大幅に向上します。詳細については、「V8 パーサー — 設計、課題、および JavaScript の解析の向上」を参照することをお勧めします。 V8 では、起動時に JavaScript のコンパイル プロセスをバックグラウンド スレッドで実行できるようにする適切なオフロード メカニズムも探しています。 プリコンパイル済みの JavaScript?
🎜 数年ごとに、エンジンがプリコンパイルされたスクリプトを処理するための何らかのメカニズムを提供すべきであると提案する人がいます。言い換えれば、開発者はビルド ツールまたは他のサーバー側ツールを使用してスクリプトをバイトコードに変換し、ブラウザーがこれらのバイトコードを直接実行できるようにするということです。それ。私の個人的な観点からすると、バイトコードを直接送信するとパッケージ本体が大きくなり、必然的に読み込み時間が長くなり、安全に実行できることを確認するためにコードに署名する必要があります。 V8 に対する当社の現在の立場は、起動時間を短縮するために上記の内部再解析を可能な限り回避することですが、プリコンパイルには追加のリスクが伴います。ただし、V8 は現在コンパイル効率の向上と、起動効率を向上させるための Service Worker キャッシュ スクリプト コードの使用の促進に重点を置いていますが、この問題について一緒に議論することは歓迎します。 BlinkOn7 では Facebook および Akamai とプリコンパイルについても議論しました。 🎜🎜JS の最適化を最適化する🎜🎜 V8 のような JavaScript エンジンは、完全な解析を実行する前にスクリプト内のほとんどの関数を事前解析します。これは主に、ほとんどのページに含まれる JavaScript 関数がすぐには解析されないためです。 🎜🎜🎜🎜プリコンパイルは、ブラウザーの実行に必要な最小限の関数セットのみを処理することで起動時間を短縮できますが、このメカニズムは IIFE に直面すると実際には効率を低下させます。エンジンはこれらの関数の前処理を回避したいと考えていますが、optimize-js のようなライブラリよりも効果がはるかに低くなります。 optimize-js はエンジンの前にスクリプトを処理し、すぐに実行される関数に括弧を挿入して実行を高速化します。この種の前処理は、Browserify と Webpack で生成されたパッケージ本体に非常に優れた最適化効果をもたらします。パッケージ本体には、すぐに実行できる多数の小さなモジュールが含まれています。この小さなトリックは V8 が使用したいものではありませんが、対応する最適化メカニズムを現段階で導入する必要があります。
起動フェーズのパフォーマンスは非常に重要であり、解析、コンパイル、実行時間が遅いと、Web ページのパフォーマンスのボトルネックになる可能性があります。この段階でページが費やす時間を評価し、最適化するための適切な方法を選択する必要があります。今後もV8の始動性能向上に全力で取り組んでまいります!
上記は JavaScript 起動時のパフォーマンスのボトルネック分析と解決策の内容です。さらに関連する内容については、PHP 中国語 Web サイト (www.php.cn) に注目してください。