◆ 数千件表示しても快適に動くページもあれば数百件で重いページもある
  ◆ 幅や高さ固定だと要素多くても割と快適
  ◆ flexbox や grid 使ったり端で折り返しなど 中身を計算しないとサイズが決まらないのは重い
    ◆ 固定できず便利な自動サイズ調整に任せるなら諦める
◆ 上に追加したら下全部が再計算されてるみたい
  ◆ 下への追加はすぐなのに上への追加は固まる
◆ 体感 ページ全体よりオーバーフローをスクロールにしてる部分スクロールのほうが重い
◆ IntersectionObserver で表示されるもののみボックス内部を描画するようにしたら逆に重い
  ◆ 基本ブラウザに任せて変なことしないほうが良さそう

重いページ

ブラウザって普段は結構快適ですが DOM が重くなるとやっぱり重くなります
ブログの見出しページや Twitter のツイートが並んでる画面みたいに 1 つ 1 つにある程度の情報があるブロックが大量に並んでいる画面があります
100 個くらいなら軽いのですが 500 とか 1000 まで来ると スクロールがかなり重くてカクカクします

500 個も表示する必要ある?と言われればめったにないのですが いわゆる無限スクロールかつ定期的に新規データを確認して データがあれば自動で一番上に追加なので 何かを探しながらスクロールを続けたり放置してると数百くらいは普通に行きます
1000 超えると 「多すぎるのでリセットしました」 とかメッセージを出してリストをクリアしてもいいかなと思うのですが 1000 くらいまでは快適にしたいなーと思います
ですがそこそこスペックある PC の Chrome でも重いのですよね

重かったりそうでなかったり

似たような状況でも重いページや軽いページがあるので調べてみました

オーバーフロースクロールは重い?

ヘッダやフッターやサイドバーなどいろいろつけてメインコンテンツを真ん中部分において css の overflow を使って要素内のスクロールにすると ページ全体にスクロールバーがあるページに比べるとけっこう重い気がします
ページや表示項目自体が違うので計測結果じゃなくて体感ですが ページ全体に単純に表示してるページだと 1000 件や 2000 件あっても快適に表示されてます

決まったブロックの繰り返しではないですが ドキュメントで全部を 1 ページにまとめた重いページがあります
しかしそれは最初のロードだけでそれ以降のスクロールは快適です
ページの高さ的にはこっちのほうが遥かに上なのに スクロールがカクつくことは全然ないです
whatwg の HTML 仕様書ECMAScript の仕様書なんて重いドキュメントページの上位の方だと思いますが これらのページでもロード後のスクロールはなめらかです

基本 HTML はドキュメントですし ページ全体のスクロールが最優先なのはわかりますが body の高さはウィンドウの高さと一緒なドキュメントというよりアプリケーション風なページを作ってるとやっぱりスクロールするところにスクロールバーを起きたいですしヘッダーなどを fixed で固定も作りづらいです

flexbox などは重い?

他には 100 や 1000 どころか 10000 くらいの要素があっても軽いページもあります
flexbox とか grid とか使っていたり 自動折り返しでウィンドウサイズで高さが変動とかあると重いですがそういうのがないと高速です

flexbox などがあると必要以上に再計算されるようです
すでに要素が 1000 近くあって重いページに要素を新規追加するとき 最後に追加だと一瞬なのに最初に追加すると数十秒固まることもあります
1 番上に 1 つ要素増えただけなので 追加された要素の高さ分だけ それ以降の要素を下にずらせば良いだけなのですが それ以降の要素のボックスサイズをすべて再計算してるようでした

仮想スクロール

軽くするための仮想スクロールというのも最近は見かけます
チャットツールとか chrome で ssh 接続して shell を使うツールのログとかがそういう仕組みでした
基本的には全体のサイズ分の高さを 1 つの div で確保してそのスクロール位置に応じて画面上に表示される要素だけを position: absolute で座標指定して表示するものです

ただこの機能を使うには条件があって 高さが固定でないといけません
ウィンドウ幅に応じて折り返し位置が変わって高さが変わったり 表示する文章の長さに応じて要素ごとに高さが違ったりすると難しいです
全体の高さや個々の高さを知るために全部のボックスサイズの計算が必要になって 結局重くなります

高さが固定になると使えるところが限られるのですよね
一行の表示のものとか 詳細画面があるからオーバーフローは 「...」 にして良いとかそういう場合だけです
並んでる部分にすべての情報があって詳細表示もないのだと 高さ固定はほぼ無理なので仮想スクロールみたいなことをしないで素直に並べるしかないです

幅を固定してしまえるなら 初回のみ高さを計算して それを style に設定しておくことで それ以降は内部のコンテンツの位置やサイズを計算しなくても style に指定された height だけ見ることができるので無駄が減りそうです
これで仮想スクロールができそう?

画面内だけ表示させてみる

DOM に存在する要素が多いと重くなる気がしますが ツリー上にあってもそれだけだと大して影響しません
重いのは CSS を適用した表示のためのレイアウト計算やレンダリング処理です
それに比べると 表示されない HTML 要素が 10000 個くらいあったところでほぼ無視できます

表示していても 中身が空で高さが固定のブロック要素ならレイアウト計算も複雑にならないのでほとんど遅くなりません
そう考えると 仮想スクロールを作るときに画面上に表示される要素のみを DOM にアタッチして position: absolute で座標指定なんて複雑なことしなくても 普通に全部の要素を並べておいて 画面に表示される要素以外は中身を空にしたボックスサイズを確保するだけの要素でもいい気がします

というわけで IntersectionObserver を使って 画面に入ってたら中身を表示して画面外に出たら中身を非表示にしてみました

<!doctype html>
<script>
const observer = new IntersectionObserver(
entries => {
for (const entry of entries) {
if (entry.intersectionRatio > 0) {
entry.target.render()
} else {
entry.target.unrender()
}
}
},
{ threshold: 0 }
)

customElements.define(
"v-box",
class extends HTMLElement {
constructor() {
super()
this._root = null
this.attachShadow({ mode: "open" })
this._slot = document.createElement("slot")
}

connectedCallback() {
observer.observe(this)
}

disconnectedCallback() {
observer.unobserve(this)
}

render() {
this.shadowRoot.append(this._slot)
}

unrender() {
this._slot.remove()
}

get bwidth() {
return this._box_width
}

set bwidth(value) {
this._box_width = value
this.style.width = this._box_width + "px"
}

get bheight() {
return this._box_height
}

set bheight(value) {
this._box_height = value
this.style.height = this._box_height + "px"
}
}
)
</script>

<!-- use -->
<style>
v-box {
display: block;
height: 60px;
}

.container span {
margin: 5px;
width: 300px;
}
</style>
<div id="d"></div>

<script>
for (let i = 0; i < 50000; i++) {
const b = document.createElement("v-box")
b.innerHTML = `
<div class="container">
<span>${i}</span><span>${Math.random()}</span><span>BQIE0!zMK</span>
</div>
<hr>
`
d.append(b)
}
</script>

v-box 要素が並べるボックスです
画面内に入ったり出たりすると IntersectionObserver から render/unrender メソッドを呼び出されます
これらのメソッドで slot 要素を append したり remove したりして子要素の表示状態を切り替えてます

50000 件表示して見たのですが 普通に重いです
良さげな内側に入れる要素がなくて span 並べただけというのもあってか IntersectionObserver なしで常に表示したままのものと比べると 常に表示したままのほうがはるかに快適です
要素が多いと IntersectionObserver が遅くなるのか スクロールしてもしばらく真っ白で表示されなかったりします

考えてみたら こういうことは自分でやらなくてもブラウザが最適化で内部でやってそうですしね
見えないところで必要ない処理は自動でできる限りやらなくなってるのでしょう
今回は 中身がシンプルすぎるから余計な表示切り替えをしたほうが遅いですが各要素内で flexbox や grid などをいっぱい使ったり画像表示などもしたりと重そうな処理になってると 画面内に位置する数個のみ表示するほうが軽くなったりすることもあるかもしれません
一番上に要素追加で下全部が再計算される場合は効果高そうです

まとまらない

適当にあれこれ試して何したいのかよくわからなくなってきました

結局のところ余計なことはせずブラウザ任せにしたほうが良いと思います
変化の大きいブラウザなんて 内部の最適化もバージョン上がったら変わってることも普通にありそうですし 変に頑張っても無駄にしかならない気がします
それにページを作る側でできることよりブラウザのレンダリング処理のレベルで最適化したほうがはるかに効果高いですし
とりあえず要素が大量にあって重くなるなら サイズ固定できるところは固定してサイズを指定するのと 子要素を計算しなくてもサイズが決まるように親要素側で指定するくらいでいいと思います
flexbox など便利な機能を使う分 重くなるものは仕方ないものです