無限スクローラーの複雑さ

要約: DOM 要素を再利用して、ビューポートから遠い DOM 要素を削除します。プレースホルダを使用して遅延データを考慮します。無限スクローラーのデモコードをご覧ください。

インターネット上に無限のスクローラーが表示されます。Google Music のアーティストリスト、Facebook のタイムライン、Twitter のライブフィードもリストです下方向にスクロールすると、一番下までスクロールすると、不思議な形で新しいコンテンツが表示されます。ユーザーにとってはシームレスなエクスペリエンスであり、アピールが見つけやすくなります。

しかし、無限スクローラーの背後にある技術的課題は、思っている以上に難しいです。The Right ThingTM は、さまざまな問題に直面します。まず、コンテンツによってフッターが押し出されて、フッターのリンクが実際にはアクセスできなくなるという単純なことから始まります。しかし、問題はますます難しくなっています。ユーザーがスマートフォンを縦向きから横向きに変えたときのサイズ変更イベントをどのように処理しますか?また、リストが長くなりすぎたときに、スマートフォンの処理が止まってしまうのを防ぐにはどうすればよいでしょうか。

最適なもの TM

これこそが、パフォーマンス基準を維持しながら、再利用可能な方法で上記のすべての問題に取り組む方法を示すリファレンス実装を思いついた理由だと考えました。

この目標を達成するために、DOM リサイクル、tombstone、スクロールのアンカーという 3 つの手法を使用します。

デモケースは、メッセージをスクロールできるハングアウトのようなチャット ウィンドウです。まず必要なのは、チャット メッセージの無限ソースです。技術的には、世の中の無限スクローラーは真に無限ではありませんが、こうしたスクローラーに取り込むことのできるデータ量があれば十分です。わかりやすくするために、一連のチャット メッセージをハードコードし、メッセージ、作成者、ときどき画像の添付ファイルをランダムに選択し、人為的な遅延を少し加えて実際のネットワークと同じように動作させます。

Chat アプリのスクリーンショット

DOM のリサイクル

DOM リサイクルは、DOM ノード数を少なく抑えるための手法として十分に活用されていません。一般的に、新しい DOM 要素を作成するのではなく、作成済みの DOM 要素を使用して画面外に配置します。もちろん、DOM ノード自体は安価ですが、それぞれがメモリ、レイアウト、スタイル、ペイントに追加の費用がかかるため、無料ではありません。ウェブサイトの DOM が大きすぎて管理できない場合、ローエンド デバイスではまったく使用できない場合でも、速度が大幅に低下します。また、スタイルを再レイアウトしたり、スタイルを再適用したりするたびに(ノードでクラスが追加または削除されるたびにトリガーされるプロセス)、DOM が大きくなるとコストが高くなることに注意してください。DOM ノードをリサイクルするということは、DOM ノードの総数を大幅に減らし、これらすべての処理を高速化することを意味します。

1 つ目の課題はスクロールそのものです。任意の時点で DOM の利用可能なすべてのアイテムのうち、ごく一部しか利用できないため、ブラウザのスクロールバーに、理論上存在するコンテンツの量を適切に反映させる別の方法を見つける必要があります。1 x 1 ピクセルの標識要素と変換を使用して、アイテムを含む要素(ランウェイ)を目的の高さにします。ランウェイのすべてのレイヤをそれぞれのレイヤに昇格させ、ランウェイ自体のレイヤを完全に空にします。背景色はなし滑走路のレイヤが空でない場合、ブラウザによる最適化の対象とならず、高さが数十万ピクセルの高さのテクスチャをグラフィック カードに保存する必要があります。モバイルデバイスでは絶対に不可能です

スクロールするたびに、ビューポートが滑走路の終点に十分に近づいているかどうかを確認します。その場合、センチネル要素を移動し、ビューポートから出たアイテムをランウェイの下部に移動することでランウェイを拡張し、新しいコンテンツを入力します。

ランウェイ Sentinel

逆方向にスクロールする場合も同様です。ただし、この実装ではランウェイを縮小しないため、スクロールバーの位置は一定に保たれます。

トゥームストーン

前述のように、Google ではデータソースが現実世界のような動作をするよう努めています。ネットワーク レイテンシなど、あらゆるデータが対象です。つまり、ユーザーが軽快なスクロールを使用している場合、データがある最後の要素を見越してスクロールするのも簡単です。その場合、プレースホルダである Tombstone アイテムを配置します。データが到着すると、実際のコンテンツを持つアイテムに置き換えられます。Tombstone はリサイクルされ、再利用可能な DOM 要素用の別のプールがあります。そのため、Tombstone から、コンテンツが入力されたアイテムに適切に移行できるようにする必要があります。そうしないと、ユーザーにとって非常に不快なだけでなく、何に注目していたかを見失ってしまう可能性があります。

古墳。とても石だ。えー びっくり。

ここで興味深いのは、アイテムや添付される画像ごとにテキストの量が異なるために、実際のアイテムの高さが Tombstone アイテムよりも大きくなる可能性があることです。この問題を解決するには、データを取得してビューポートの上に Tombstone が置き換えられるたびに現在のスクロール位置を調整し、スクロール位置をピクセル値ではなく要素に固定します。このコンセプトは、スクロールのアンカーと呼ばれます。

スクロールのアンカー

スクロールのアンカーは、Tombstone が置き換えられるときと、ウィンドウのサイズが変更されたとき(デバイスが反転しているときにも呼び出されます)の両方で呼び出されます。まず、ビューポートで一番表示されている要素が何かを特定する必要があります。この要素は部分的にしか表示できないため、ビューポートが始まる要素の上部からのオフセットも保存します。

スクロールのアンカーの図。

ビューポートのサイズが変更され、ランウェイが変更された場合、視覚的にユーザーと同じに見える状況を復元できます。勝利!サイズ変更されたウィンドウを除き、各アイテムの高さは変化する可能性があります。では、固定されたコンテンツをどこまで下に配置すべきかを知るにはどうすればよいでしょうか。いいえ。これを確認するには、アンカーされたアイテムの上にすべての要素をレイアウトし、すべての高さを合計する必要があります。サイズ変更後に大幅な一時停止が発生する可能性があるため、これは望ましくありません。代わりに、上記のアイテムがすべて Tombstone と同じサイズであると想定し、それに応じてスクロール位置を調整します。要素がランウェイにスクロールされると、スクロール位置が調整され、実際に必要なときにレイアウト作業が実質的に延期されます。

レイアウト

重要な点はスキップしてあります。レイアウトです。通常、DOM 要素がリサイクルされるたびに、滑走路全体が再レイアウトされるため、1 秒あたり 60 フレームという目標を大幅に下回ります。これを回避するために、レイアウトの負担を自社に負わせて、絶対位置にある要素を使用して変換を行います。これにより、実際には空のスペースしかないのに、滑走路より上にあるすべての要素がスペースを占有していると想定できます。レイアウトを自分で行うため、各アイテムの終了位置をキャッシュに保存でき、ユーザーが後方にスクロールしたときにすぐにキャッシュから正しい要素を読み込むことができます。

理想的には、アイテムは DOM にアタッチされたときに 1 回だけ再ペイントされ、ランウェイに他のアイテムが追加または削除されても混乱してしまいます。ただし、これは最新のブラウザでのみ可能です。

初期段階の微調整

最近、Chrome に「CSS Containment」のサポートが追加されました。この機能により、デベロッパーは、要素がレイアウトとペイント処理の境界であることをブラウザに伝えることができます。ここでは自分たちでレイアウトを作成するので、コンテインメントの主要なアプリケーションです。ランウェイに要素を追加するときは常に、再レイアウトによって他のアイテムに影響を与える必要がないことがわかっています。そのため、各アイテムは contain: layout になります。また、ウェブサイトの他の部分には影響を与えないように、ランウェイ自体にもこのスタイル ディレクティブを適用する必要があります。

また、要素のリサイクルを開始して新しいデータを読み込むのに十分な範囲をユーザーがスクロールしたことを検出するメカニズムとして、IntersectionObservers を使用することも検討しました。ただし、IntersectionObserver は(requestIdleCallback を使用する場合と同様に)高レイテンシになるよう指定されているため、IntersectionObserver を使用しない場合よりも応答性が下がる可能性があります。スクロール イベントは「ベスト エフォート」ベースでディスパッチされるため、scroll イベントを使用する現在の実装でもこの問題が発生します。最終的には、Houdini のコンポジタ ワークレットが、この問題に対する忠実度の高いソリューションとなるでしょう。

完璧とは言えない

DOM リサイクルに関する現在の実装は、実際に画面上にある要素を考慮せず、ビューポートを通過するすべての要素を追加するため、理想的ではありません。つまり、非常に短時間でスクロールすると、Chrome のレイアウトとペイントに多大な労力を費やすことになります。最終的には背景しか表示されなくなります。世界の終わりではありませんが、改善の余地があることは確かです。

優れたユーザー エクスペリエンスと高パフォーマンス基準を兼ね備える際に、どれほど難しいシンプルな問題に直面するのかをご理解いただければ幸いです。プログレッシブ ウェブアプリがスマートフォンの主要なエクスペリエンスとなるにつれ、このことはますます重要になります。ウェブ デベロッパーは、パフォーマンスの制約を尊重するパターンの使用に投資し続ける必要があります。

コードはすべてリポジトリで確認できます。Google は、再利用できるように最善を尽くしましたが、実際のライブラリとして npm や別のリポジトリとして公開することはありません。主に教育を目的としている。