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

要約: DOM 要素を再利用し、ビューポートから遠く離れた要素を削除します。プレースホルダを使用して、遅延したデータを考慮します。無限スクローラーのデモコードは次のとおりです。

無限スクロールはインターネットのあらゆる場所で表示されます。Google Music のアーティスト リスト、Facebook のタイムライン、Twitter のライブフィードも同様です。下にスクロールすると、一番下までスクロールする前に、新しいコンテンツが突然表示されます。ユーザーにとってはシームレスなエクスペリエンスであり、その魅力は簡単に理解できます。

ただし、無限スクロールの背後にある技術的な課題は、見た目よりも難しいものです。The Right Thing™ を行おうとすると、さまざまな問題が発生します。フッターのリンクがコンテンツによって押し下げられ、事実上アクセスできなくなるなど、シンプルなことから始まります。しかし、問題は難しくなります。ユーザーがスマートフォンを縦向きから横向きに切り替えたときに、サイズ変更イベントをどのように処理しますか?また、リストが長くなりすぎたときに、スマートフォンが停止するのをどのように防ぎますか?

The right thing™

そこで、パフォーマンス基準を維持しながら、これらの問題を再利用可能な方法で解決する方法を示すリファレンス実装を考案するのに十分な理由があると考えました。

この目標を達成するために、DOM リサイクル、トゥームストーン、スクロール アンカーの 3 つの技術を使用します。

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

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

DOM の再利用

DOM の再利用は、DOM ノード数を少なく保つために十分に活用されていない手法です。一般的な考え方としては、新しい DOM 要素を作成するのではなく、画面外にすでに作成されている DOM 要素を使用します。DOM ノード自体は安価ですが、無料ではありません。各ノードはメモリ、レイアウト、スタイル、ペイントで追加のコストを発生させます。ウェブサイトの DOM が大きすぎると、ローエンド デバイスでは、完全に使用できなくなることはないにしても、著しく動作が遅くなります。また、クラスがノードに追加または削除されるたびにトリガーされる、スタイルの再レイアウトと再適用というプロセスは、DOM が大きくなるほどコストが高くなることにも注意してください。DOM ノードを再利用することで、DOM ノードの総数を大幅に減らし、これらのプロセスをすべて高速化できます。

最初のハードルはスクロール自体です。DOM には、利用可能なすべてのアイテムの小さなサブセットしか存在しないため、ブラウザのスクロールバーに理論上のコンテンツ量を正しく反映させる別の方法を見つける必要があります。変換を使用して 1 ピクセル × 1 ピクセルのセンチネル要素を使用し、アイテムを含む要素(ランウェイ)に目的の高さを強制的に設定します。滑走路内のすべての要素を独自のレイヤに昇格させ、滑走路自体のレイヤが完全に空になるようにします。背景色なし。ランウェイのレイヤが空でない場合、ブラウザの最適化の対象にはならず、高さが数十万ピクセルのテクスチャをグラフィック カードに保存する必要があります。モバイル デバイスでは確実に実現できません。

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

Runway Sentinel Viewport

逆方向のスクロールについても同様です。ただし、実装ではランウェイを縮小しないため、スクロールバーの位置は一貫性を保ちます。

Tombstone

前述したように、データソースを現実世界の何かに似た動作にしようとしています。ネットワーク レイテンシなど、すべてを含みます。つまり、ユーザーがフリック スクロールを使用すると、データがある最後の要素を簡単にスクロールして超えることができます。その場合、データが到着すると実際のコンテンツを含むアイテムに置き換えられるプレースホルダである、削除済みアイテムが配置されます。墓石も再利用され、再利用可能な DOM 要素用の別のプールがあります。これがないと、墓石からコンテンツが入力されたアイテムへのスムーズな移行ができません。スムーズな移行ができないと、ユーザーは非常に不快に感じ、実際に注目していたものを忘れてしまう可能性があります。

このような墓。非常に石が多い。ワオ

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

スクロール アンカー

スクロール アンカーは、墓石が置き換えられるときと、ウィンドウのサイズが変更されるとき(デバイスがフリップされるときにも発生します)の両方で呼び出されます。ビューポート内で最も上にある表示要素を特定する必要があります。その要素は部分的にしか表示されない可能性があるため、ビューポートが始まる要素の上部からのオフセットも保存します。

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

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

レイアウト

重要な詳細情報であるレイアウトについて説明していませんでした。DOM 要素を再利用するたびに、通常は滑走路全体が再レイアウトされるため、目標の 60 フレーム/秒を大きく下回ってしまいます。これを回避するため、レイアウトの負担を軽減し、変換を伴う絶対位置の要素を使用します。このようにして、実際には空きスペースしかない場合でも、滑走路の上にあるすべての要素がまだスペースを占有しているかのように見せることができます。レイアウトは自分たちで行うため、各アイテムの最終的な位置をキャッシュに保存し、ユーザーがスクロール バックしたときにキャッシュから正しい要素をすぐに読み込むことができます。

理想的には、アイテムが DOM に追加されたときに一度だけ再描画され、滑走路内の他のアイテムの追加や削除の影響を受けないようにします。これは可能ですが、最新のブラウザでのみ可能です。

最先端の調整

最近、Chrome で CSS Containment のサポートが追加されました。この機能を使用すると、デベロッパーはブラウザに、要素がレイアウトとペイントの境界であることを伝えることができます。ここではレイアウトを自分で行うため、封じ込めの最適なアプリケーションです。ランウェイに要素を追加するたびに、他のアイテムはレイアウトの再計算の影響を受けないことがわかります。そのため、各アイテムは contain: layout を取得する必要があります。ウェブサイトの他の部分に影響を与えないように、ランウェイ自体にもこのスタイル ディレクティブを適用する必要があります。

また、IntersectionObservers を使用して、ユーザーが要素の再利用と新しいデータの読み込みを開始するのに十分な距離までスクロールしたことを検出することも検討しました。ただし、IntersectionObserver は高レイテンシ(requestIdleCallback を使用しているかのように)になるように指定されているため、IntersectionObserver を使用すると、使用しない場合よりもレスポンスが遅く感じられる可能性があります。scroll イベントを使用する現在の実装でも、スクロール イベントは「ベスト エフォート」ベースでディスパッチされるため、この問題が発生します。最終的には、Houdini の Compositor Worklet がこの問題に対する忠実度の高いソリューションとなります。

まだ完璧ではありません

現在の DOM リサイクルの実装は、実際に画面に表示されている要素だけでなく、ビューポートを通過するすべての要素を追加するため、理想的ではありません。つまり、非常に速くスクロールすると、レイアウトとペイントの処理が Chrome に集中し、処理が追いつかなくなるということです。背景しか表示されなくなります。この世の終わりではありませんが、改善すべき点があるのは確かです。

優れたユーザー エクスペリエンスと高いパフォーマンス基準を組み合わせようとすると、単純な問題がどれほど難しくなるかをご理解いただけたかと思います。プログレッシブ ウェブアプリがモバイルのコア エクスペリエンスになるにつれて、この点はますます重要になり、ウェブ デベロッパーはパフォーマンスの制約を尊重するパターンの使用に投資し続ける必要があります。

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