HN 日本語サマリー

← 一覧へ戻る
Web開発

ブラウザのメインスレッドは高コストである

The Browser's Main Thread Is Expensive (kciter.so)

381 pointsby kciter134 コメント

要約

ブラウザのメインスレッドは、JavaScriptの実行、レンダリング、イベント処理など、多くの重要なタスクを担っており、そのブロックはユーザー体験を著しく低下させます。この記事では、この貴重なリソースを賢く使うための戦略として、メインスレッド内での作業分割やタイミング調整、あるいはコンポジターやWeb Workerへのタスクオフロードといった手法を解説しています。

全文翻訳

フロントエンド最適化と聞いて何を思い浮かべますか? ほとんどの場合、ネットワークリクエストの削減、バンドルサイズの縮小、キャッシュの有効活用などが挙げられます。さらに進めば、再レンダリングの抑制やリソースのロードタイミングの調整といったことも考えられます。メインスレッドが問題になることは、ほとんどの画面では稀なため、通常は話題に上りません。しかし、インタラクションが多く、データがリアルタイムでストリーミングされ、スクロール、アニメーション、入力処理が絡み合うような画面では状況が変わります。ネットワークやバンドルサイズでどれだけ節約しても、メインスレッドがブロックされると画面はフリーズしてしまいます。 ウェブサイトでスクロールが時々カクついたり、ボタンの反応が少し遅れたり、検索ボックスに入力した文字が半拍遅れて表示されたりした経験があるかもしれません。それは不快になるほどではありませんが、微妙に神経に障ります。そのようなジャーク(jank)は、メインスレッドがブロックされている状態の現れです。開発者としてこのようなジャークに遭遇した場合、通常の反応は「自分のコードが遅いのか?」と考え、アルゴリズムを分解したり、無駄な計算を探したりすることです。しかし、ほとんどの場合、コードの速度自体が問題なのではありません。コードが遅いのではなく、たまたまメインスレッドを占有しているコードなのです。 ブラウザには複数のスレッドがありますが、コードからアクセスできるほとんどすべてのものがメインスレッドに集中しています。計算、レンダリング、イベント処理、ネットワーク応答処理、そしてフレームワークの内部処理もすべてそこで実行されます。一つのリソースに、膨大な量の作業が集中しているのです。 ブラウザのメインスレッドは高コストです。ほとんどの場合、問題を引き起こしませんが、野心的なことをしようとすると、メインスレッドの扱いは重要な部分になります。この記事は、その高コストなリソースをどのように扱うかについての記事です。 メインスレッドは何をするのか? まず、メインスレッドが実際に行っていることを理解することから始めましょう。その作業は、大きく分けて2つのカテゴリに分類されます。 第一に、JavaScriptの実行です。私たちが書くコード、イベントハンドラ、タイマー、ネットワーク応答コールバック、そしてフレームワークの内部処理はすべてここで実行されます。これらのタスクは、キューに入った順序で、隙間があれば実行され、画面のリフレッシュサイクルとは無関係に処理されます。 第二に、画面の描画です。DOMやスタイルが変更され、画面の更新が必要になった場合、ブラウザはフレームを生成するために、およそ以下のステップを順に実行します。 requestAnimationFrame コールバックの実行 - フレーム描画直前に実行されるよう登録されたJavaScript スタイルの計算 - 各要素の最終的なCSS値を計算する レイアウト - 各要素の位置とサイズを計算する(リフローとも呼ばれる) ペイント - どの色でどこに描画するかを記述するペイントコマンドを生成する 何も変更がなければ、これらのステップは完全にスキップされるため、必ずしも毎フレーム実行されるわけではありません。最終的なコンポジット(compositing)ステップ、つまり生成された出力を画面に組み立てる処理だけが、コンポジター・スレッドに引き渡されます。 つまり、画面を描画するパイプラインの前半のほとんどは、メインスレッドの責任なのです。 画面更新のためのレンダリングパイプライン 画面をスムーズに見せるためには、フレームが表示のリフレッシュレートで描画される必要があります。最も一般的な60Hzディスプレイでは、これは毎秒60フレーム、つまり約16.6ミリ秒ごとに1フレームを意味します。そして、そのすべてを使うことはできません。ブラウザ自身の処理コストを差し引くと、実質的な予算は通常約10ミリ秒と見なされ、120Hzデバイスでは予算自体が半分になります。 問題は、上記の2種類の作業が、同じスレッド上で単一のラインに並んでいることです。JavaScriptはシングルスレッドのイベントループモデルを中心に設計されています。メインスレッドは一度に1つのタスクを処理し、そのタスクが実行されている間は、他の何も起こりえません。もし1つのJavaScript関数が200ミリ秒実行されたら、その200ミリ秒の間、ブラウザは画面を再描画したり、ユーザーからのクリックを受け付けたりすることができません。約10ミリ秒のフレーム予算に対して、これは致命的な時間です。 このように長く実行され、メインスレッドを占有するタスクはロングタスクと呼ばれ、一般的に50ミリ秒を超えるものは問題と見なされます。 言葉だけでは伝わりにくいので、体感してみましょう。以下のデモでは、ボタンを押すとJavaScriptが一時的にメインスレッドを掴みます。 JSアニメーション メインスレッド · rAF CSSアニメーション コンポジター · transform 0.5秒ブロック 2秒ブロック ボタンを押すと、JSアニメーションとタイピングがフリーズしますが、CSSアニメーションは回転し続けます。 ボタンを押すと、JSアニメーションが停止し、入力フィールドへのタイピングも無反応になります。一方、CSSアニメーションは実行され続けます。この違いの由来については後ほど触れます。現時点で覚えておくべきことは、メインスレッドを長時間占有することは、画面をフリーズさせることと同じであるということです。 これはWebパフォーマンス指標に直接つながります。INP(Interaction to Next Paint)は、ユーザーが何かを行った後に画面が応答するまでの時間を測定し、TBT(Total Blocking Time)は、ページ読み込み中にメインスレッドがブロックされていた合計時間を測定しますが、これらは本質的にメインスレッドがどれだけブロックされていたかを示す方法です。 パフォーマンス最適化の大部分は、この1つのスレッドをどれだけ注意深く使うかという問題です。それを注意深く使う方法は、大きく分けて2つのファミリーに分けられます。1つは、メインスレッドの時間配分を内部からうまく行うことです。もう1つは、作業をメインスレッドから完全に外部に送信することです。順に見ていきましょう。 高コストなリソースを賢く使う 最初のファミリーは、メインスレッド上に留まりつつ、その時間を賢く使うことです。これには4つの主要な動きがあります。 長すぎる実行時間を分割するにはどうすればよいか? 頻繁に実行される作業をグループ化するにはどうすればよいか? 複数のタスクの中で、どれを優先すべきか? 今すぐ必要ない作業を延期するにはどうすればよいか? これらを、分割(splitting)、バッチ処理(batching)、優先順位付け(prioritizing)、遅延(deferring)と呼びます。最初の2つはタスクのサイズを形成し、後の2つはタイミングを決定します。4つのうち、分割は他のすべての基礎となります。タスクには境界線が必要であり、その間に何を配置するか、何を後回しにするかを決定できます。したがって、分割から始めます。 分割 ライブストリームのチャットペインを想像してみてください。人気のストリームでは、チャットは毎秒数百メッセージに達することがあります。そのような環境では、メッセージは一度に1つずつ丁寧には届きません。トラフィックが急増すると、サーバーはそれらを数十件の塊で送信し、部屋に入った瞬間に、バックログにあった数百件のメッセージが一気に降りてきます。 その塊が到着したときに、一度にすべてレンダリングしたらどうなるでしょうか? 描画するメッセージごとにDOM作成、スタイル計算、レイアウト、ペイントが伴い、それらの数百回のイテレーションが単一のタスク内で連続して実行されます。その間、自分のメッセージを入力しようとしているユーザーは、入力フィールドのスタッターに悩まされ、画面上の他のすべてのアニメーションもカクつきます。他の人のチャットがメインスレッドを独占し、あなたのチャットの邪魔をしています。 その修正策は、先ほど述べたことです。塊を小さなピースにカットし、ピースの間で、一時的にメインスレッドの制御を返します。そのギャップで、ブラウザはキューに入れられていた画面更新や入力処理を追いつかせることができます。 以下のデモは、ストリーミングチャットペインをシミュレートしています。「チャットをフラッドする」を押すと、メッセージが降り注ぎ始めます。画面上部のスムージングゲージとFPSを見ながら、入力フィールドにタイプしてみてください。「即時レンダリング」モードと「イールドレンダリング」モードを比較してください。 スムージングインジケーター(JSアニメーション) 60 fps 即時レンダリング イールドレンダリング チャットをフラッドする 「即時レンダリング」モードでは、メッセージが到着するたびにDOMが操作されるため、チャットがフラッドしている間はFPSが急激に低下し、ゲージがカクつき、入力フィールドが遅延します。よく見ると、