3D の読み込み予算 — チャンク分割・可視になってから読み込み・止めどき
3D ページの費用は三つあります。ダウンロード、初回フレームのコンパイル、毎フレームの描画。それぞれに別々の予算を決めないと、見栄えが引っかかりに変わります。
3D ページの費用は三つに分かれ、それぞれ別に数えなければなりません。
一つ目 — ダウンロード
浮島のシーンチャンクは 582.9 KB(gzip で約 150 KB)。この代金は浮島ページを開いた人だけが払い、しかも本当に必要になった瞬間に払うべきです。三つの関門を置いています。
- ページ単位で分割。HTML にあるのは 1.2 KB の起動スクリプトだけで、シーンは動的な
import()で届きます。HTML に書かず、プリロードもしません。 - 可視になってから。
IntersectionObserverが 200px の余裕を持って舞台を見張ります。スクロールして来なければ一バイトも落としません。 - アイドルになってから。可視になった後、さらに
requestIdleCallbackを待ちます(Safari にはないので 200ms の遅延に退化)。初回画面の CSS、フォント、スクリプトと帯域を争いません。
三つを合わせた結果、他の四百ページ余りに three は入らず、浮島ページの初回画面とも競合せず、舞台に届く前に離れれば費用はゼロです。
二つ目 — 初回フレームのコンパイル
シェーダーのコンパイルは最初の描画で起きます。放置すると、骨組み、引っかかり、そしてシーン、という順番で見えます。そこでマウント前にコンパイルします。
renderer.compile(scene, camera);
読み込み中の表示はスクリプトで作らず HTML の骨組みに任せます。骨組みは文書にあり、高さは clamp() で固定されているので、スクリプトが届く前からページの高さは最終値で、下の内容が押し出されません。
三つ目 — 毎フレーム
| 項目 | 値 | 抑え方 |
|---|---|---|
| 描画呼び出し | 約 380 | インスタンス化(草・花・苔石が各一回) |
| 三角形 | 約 5 万 | ローポリとフラットシェーディング、色は頂点から |
| 影 | 一度だけ | shadowMap.autoUpdate = false |
| ピクセル比 | 上限 1.75 | 高 DPI でも 3 倍を追わない |
| ポストプロセス | ブルーム一段 | 多段にしない |
さらに prefers-reduced-motion のときはアニメーションを止め、操作は残します。自動回転は止まり、花びらは止まり、水面の time は進みませんが、ドラッグ、ズーム、視点の切り替えは使えます。これは性能最適化ではなく、設定を尊重するという話です。
失敗と降格
WebGL が起動しなければ、空白ではなく一文と再試行ボタンを出します。webglcontextlost ではアニメーションループを止めます。どちらも難しくありませんが、悪い環境でページが「壊れている」のか「使える」のかを決めます。
止めどき
3D が装飾でしかないなら(記事中の挿絵など)、最初の画面に置かず、そのためにサイト全体へ依存を足さないことです。目的地型のページだけが 150 KB を待ってもらえます。読者はそれを見るために来たのですから。
逆に、三画面スクロールしないと 3D が見えず、しかも 3D がそのページの存在理由でないなら、正しい判断は最適化ではなく作らないことです。
まとめ
3D の性能問題はレンダラの中にではなく、三つの費用に別々の予算を置いていないことにあります。ダウンロードは野放し、初回フレームは無関心、毎フレームは感覚で調整。三つを書き出してしまえば、残りはすべて局所的な話です。

コメント
…