はじめに

こんにちは、ABEMA で Web フロントエンドエンジニアをしています、20新卒の坂上です。

ABEMA には、各日付ごと、各チャンネルごとにどの時間帯にどの番組が放送されているかを確認できる 番組表 機能が存在します。
この番組表は開局当初から存在するものの、現在までそれほど変わりはない機能です。
そのため、実装にも手が加えられておらず、数年前の React のコードがそのまま残っている箇所も存在していました。
それゆえ、コードリーディングがしづらく、実装の詳細や仕様に関して詳しく知っている人がいないという状態から、番組表のパフォーマンスが芳しくないことは課題として上がっていましたが手をつけられていない状態でした。

その番組表に対して、再レンダリングの抑制と新しく提案された CSS プロパティである content-visibility を導入しました。
結果としては、Time to Interactive (TTI) の改善や、特に CPU のパフォーマンスが良くない環境において明らかに動作の滑らかさの違いがあることが確認できました。

ここではその事例について、今回の事例からわかった使いどころや導入する過程に触れながら紹介します。

content-visibility とは

content-visibility とは、プロパティを付与した要素が画面上に存在しないような、表示に必要ない場合にその要素の子要素のレンダリングフェーズ( layout、paint ) をスキップする CSS プロパティです。
そのため、適切に指定することでページ全体のレンダリングパフォーマンスの向上する効果が期待される CSS プロパティです。

新しい仕様の際に気になるのは、各ブラウザの実装状況です。
Chromium では、バージョン 85 から利用が可能となりました。
Safari は特に進展はないようですが、Firefox は “worth prototyping” の見解が示されています。

また、content-visibility は CSS Containment と密接な関わりがあります。
CSS Containment とは、ブラウザに要素が満たす制約をブラウザに伝えることにより、それに応じたレンダリングの最適化を可能にする CSS Containment という CSS プロパティです。
CSS Containment には以下の4つの種類の制約が存在します。

  • size : プロパティを付与した要素が width/height が子要素に影響されない
  • layout: プロパティを付与した要素の子要素が他の要素のレイアウトに影響を与えない
  • style: プロパティを付与した要素の子要素が他の要素のスタイルに影響を与えない
  • paint: プロパティを付与した要素の子要素がプロパティを付与した要素を越えて表示されない

ここでは各制約の列挙に留めますが、CSS Containment の詳しい解説や分かりやすい解説が次の記事にありますのでこちらを参照ください。

content-visibility では要素が viewport に入るタイミングでレンダリングを行えるように、その要素のレンダリング結果が事前に予測可能である必要があります。
そのため、content-visibility を付与する要素のうち、表示されていない場合は上記の CSS Containment 全ての制約、表示されている場合でも size 以外の全ての制約を満たさなければなりません。

size containment の制約を満たすためには、 content-visibility を付与する要素の大きさをブラウザに明示する必要があります。
よって、width / height もしくは要素の大きさとして予測される大きさを contain-intrinsic-size を用いて指定する必要があります。

そして、他の解説記事では主に縦方向のレンダリングスキップにフォーカスをあてて解説をしていることが多いですが、今回の事例では横方向で画面に必要のない要素のレンダリングをスキップすることを目的として content-visiblity を用いています。
つまり、contain-intrinsic-size に width / height を適切に指定することで、content-visibility を縦だけでなく横方向にも適用することが可能です。

ここでは事例の紹介をするにあたって前提認識を合わせることを目的にして解説を行いました。 より詳しくは 、web.dev の記事などを参照してください。

今回 abema-web での利用例

番組表に適用した背景

ここまでの説明から content-visibility の活用できそうなポイントとして、以下の2点が挙げられます。

  • 縦もしくは横にページが長く、画面外に表示されていない要素がある
  • その要素のページが長い方向の大きさ( width もしくは height ) が予測可能である

番組表は、縦軸が番組の放送時間、横軸がチャンネルもしくは日付となっており、それぞれ画面に収まるサイズではないので表示に不必要な番組のセルやチャンネル・日付のカラムが存在します。
また、カラムごとに width が指定され、番組ごとに放送時間に合わせて height が必ず指定されている構造となっています。
これらから、content-visibility が適用できそうな機能として番組表が挙がってきました。

また、番組表に対するパフォーマンス改善案として、もともと画面内の要素のみを DOM に反映する「仮想スクロール」の実装が検討されていました。
しかし、前述のとおり実装が複雑なため、リグレッションを恐れてなかなか着手できない状態でした。
一方、content-visibility であれば CSS プロパティを指定するだけなので非常に簡単です。
仮想スクロールと違って DOM には画面外の要素も存在してしまいますが、少なくともレンダリングはスキップされます。
つまり、仮想スクロールに近い機能を CSS だけで実現することができます。

各ブラウザでの対応状況

content-visibility の利用検討を始めたのは、Chrome 85 で実装されたというアナウンスを見たことがきっかけでした。
しかし、新しい技術が一つのブラウザで実装されたからといってすぐに飛びついてしまうと、その後他のブラウザベンダーが追従せずに結果的に負債になってしまう可能性もあります。
今回も実装を始める前に他のブラウザの状況を調査しました。
content-visibility の概要で述べたように Chromium では実装済みであり、Firefox でも “worth prototyping” という見解が示されていました。
このことからも Chromium のみの実装で進んでいくようなことはなさそうだったので content-visibility を導入する判断をすることができました。
また、このプロパティのあるなしでスタイルが崩れるわけでなく、対応していないブラウザにおいても無視されるだけです。
そのため、リスクが少ないことも導入しやすかった理由です。

content-visibility プロパティを付与する要素の選定

content-visibility を付与するにあたって2つのパターンを検討しました。

  1. 各番組のセル
    • それぞれ width / height が事前に一意に決まる
    • 縦方向・横方向いずれにも画面に必要のない時はレンダリングされない
  2. 各チャンネルのカラム
    • 縦方向はスクリーンいっぱい
    • 横方向は事前に一意に決まる
    • 横方向にのみ画面に必要のない時はレンダリングされない

 

この2つのケースでそれぞれ Chrome Dev Tools のパフォーマンスタブの rendering performance、Lighthouse における TTI / Total Blocking Time (TBT) のスコアを計測しました。
結果は以下のようになり、各チャンネルのカラムの方がよい結果となりました。
番組表は日付や時間によって表示されるコンテンツが異なるためほぼ同時刻( 2020/10/05 11:10 ~ 11:23 )で3回程度計測した平均値を採用しました。

指標 番組欄 チャンネル欄
TTI (s) 10.63 9.2
TBT (ms) 886.7 823.3
Rendering (ms) 661.7 546

なぜ差が出たのかを調べるため、それぞれのパターンで Chrome Dev Tools のパフォーマンスタブのフレームチャートを分析しました。
以下のキャプチャは、同じコンテンツをレンダリングしている部分のフレームチャートを表しています。

 

レンダリングの動きを表す無数の帯線が発生している
各番組のセルにおけるフレームチャート

 

レンダリングを表す紫色の帯が10本程度, 太く発生している
各チャンネルのカラムにおけるフレームチャート

 

各番組表の場合は、要素の数は多いが本当に必要な要素のみを表示するケースであり、チャンネル欄の方は縦方向の表示されていない部分も表示されてしまうケースです。
要素の数に応じて番組ごとのセルの方が紫色の帯の数が増えており、各番組表conetnt-visilibity を付与した要素ごとにレンダリング処理が走っている様子が分かります。
ここからは推測の域になりますが、番組ごとの場合の必要な分だけ表示されることによる、スキップされた要素のレンダリングコストよりも、各レンダリング処理を走らせるオーバーヘッドの方が大きくなってしまったようにも考えられます。

content-visibility のみに着目した改善結果

今回の content-visibility のリリースにあたって、番組表における React の再レンダリングの抑制と同時にリリースしてしまったために content-visibility のみでどの程度のパフォーマンス改善が行えたかを実際のプロダクション環境を基にして計測することができませんでした。
しかし、content-visibility はランタイムのパフォーマンスのみに影響しますので、ローカル環境での数値でも同じようなコンテンツであれば、おおよそプロダクション環境と同じ改善を確認できると考えられます。
それを踏まえて、ここではプロダクションビルドし、ローカル環境から本番の API を叩く形で content-visibility の有無でどの程度のパフォーマンス改善があったかをご紹介したいと思います。

CPU throttling Rendering ( ms ) TTI ( s ) TBT ( ms )
no throttling 387 → 263 ( – 96 ) 4 → 3.8 ( – 0.2 ) 290 → 273 ( – 17 )
6x slowdown 686 → 586 ( – 100 ) 8.9 → 8.4 ( – 0.5 ) 6030 → 5717 ( – 313 )

※ before → after ( after – before )

結果としては上記の表のようになりました。
いずれの環境においても content-visibility を付与したことによって数値が改善しています。
また、特にロースペックな環境で大きく影響が出ていることがわかります。

これは、ロースペックな環境では、1つ1つのレンダリング処理が大きく、時間がかかるものとなります。
その大きなレンダリング処理が回避されるので、改善の幅が大きくなっていることが考えられます。
この改善により、ロースペック環境ではスクロールの滑らかさが目に見えて改善しました。
以下は 6x slowdown 環境でのキャプチャです。

 

content-visibilityを適用する前
content-visibilityを適用した後

まとめ

ABEMA の番組表における content-visibility を用いた改善例をご紹介しました。
content-visibility を適用したことで、特にロースペックなユーザーに対して TBT が大きく改善されることにより、 TTI の改善が確認できました。
縦や横にコンテンツが長く画面外に表示されていない要素が存在し、その要素の大きさが予測可能である時には検討の価値がありそうです。
仮想スクロールの実装が難しい場合でも、CSS プロパティを指定するだけで同じような効果を手に入れることができるというのは魅力的なのではないでしょうか?