コンテンツにスキップ

段階的な HTML 更新

パフォーマンスでは、リクエストを処理するときの サーバー負荷を説明しました。ここで注目するのは、サーバー側の計測だけでは見えない 2つのコストです。通信量と、利用者が空の画面を見ながら待つ時間です。

この2つは別の問題です。描画が安くても、上に載っている1つのクエリが遅ければページが 使えるようになるまで2秒かかります。マイクロ秒で描画できても、20分前から開いている ナビゲーションバーを毎回送り直していれば、それは無駄なままです。Popcorn Web には それぞれに対応する仕組みがあり、さらに「新しいことを知るのがサーバの側」という3つ目 の場合のための仕組みがあります。

静的ファイルは計算処理がなく、長期間キャッシュでき、エッジからも配信できます。 Web フレームワークの速度を比べるうえで、基準にしやすい方式です。

それでも2回目のページ表示では、このフレームワークが送るものより厳密に劣ります。

静的サイト 2回目の表示 Popcorn Web 2回目の表示 レイアウト・ナビゲーション・フッタ — ブラウザはもう開いている 変わった分 変わった分 静的サーバはこれ以上減らせない。目の前のブラウザが何を持っているかを知る手段が無いからだ。 知っているページは差分で答えられる — そして求められればただのドキュメントを返す。

理屈はこれだけですが、なぜ成り立つのかは正確に押さえておく価値があります。静的 サーバがどのリクエストにも同じバイト列を返すのは、そうするしかないからです。 /orders?page=2 を要求する2つのブラウザのうち、一方は /orders?page=1 から来た かもしれず、他方はブックマークを開いただけかもしれません。差分を送るにはどちらか を知る必要があります。Popcorn Web は知っています。ブラウザがリクエストで告げる からで、だから答えはドキュメントではなく変わった領域になります。

この種の高速化では、別の場所にコストが移っていることも少なくありません。 クライアントサイドルータ、ハイドレーション、ビルド工程、バンドルが届くまで何も 出ないページ。ここにはそのどれもありません。ブラウザに届くのは最初のバイトから 最後までサーバ描画された HTML で、関わるスクリプトは完成したマークアップを所定の 位置へ移すもの1つだけです。それを切れば、出発点だった静的ファイルの挙動に戻ります。 詳しくは後述します。

3つが分かれるのは「サーバがブラウザの知らないことを知るのはいつか」だけです。 選ぶのに必要な問いもこれ1つです。

非同期レンダリング 1レスポンス 部分更新 2リクエスト ライブレンダリング 開いた接続1本 シェル 領域 領域 全ページ 差分 更新 更新 更新 0 1秒 2秒 3秒 4秒 例示時間 →

時刻は説明用の例ですが、出来事の違いは実際のものです。破線はブラウザが訊いた時点です。 非同期レンダリングは1回訊き、1つのレスポンスを分割して受け取ります。部分更新はもう一度 訊き、2回目にはずっと小さい応答を受け取ります。ライブレンダリングは1回訊き、薄い線で 示した接続を開いたまま、サーバに新しい情報が生まれたときだけ印の位置で受け取ります。

2番目と3番目の選び分けは、仕組みの話ではなく URL の話です。URL に置ける状態は URL に置く —— 並び順、ページ番号、絞り込み。そうすればページは共有でき、戻る ボタンも機能し、部分更新でも追加コードなしで扱えます。ライブレンダリングを使う のは、新しい情報を結びつけるリクエストがそもそも無いときです。誰もキーを叩いて いない間にサーバ側で起きたことだからです。

非同期レンダリング —— 最初の表示を早くする

Section titled “非同期レンダリング —— 最初の表示を早くする”

ページはたいてい、いちばん遅いクエリと同じだけ遅くなります。ハンドラが全部を待ち、 テンプレートが1回描画され、最後の依存が答えるまで読み手は白いタブを見ています。

{await} ブロックがこの結合を断ちます。その区画はまずフォールバックを描いて レスポンスを確定させ、値が揃ったところで完成したマークアップが同じレスポンスの中 に書き込まれてフォールバックと入れ替わります。

1本の HTTP レスポンス 200 · <head> · 待たなくていいシェル <div id="orders"> loading… </div> <div id="recs"> loading… </div> <template for="orders"> … </template> <template for="recs"> … </template> 20 ms 0.9 s 1.5 s 2つの依存は並行に走るので、レスポンスの終わりは2.4秒ではなく1.5秒。 それでも、読めるようになったのは20ミリ秒の時点だ。

大事なのは合計時間ではありません。ステータスコードとヘッド、そして揃っていた値が、 遅い処理の完了前にサーバを出ていくことです。これを得るためにハンドラは何も変わり ません。レスポンスがストリーミングになるかどうかは、組み合わせたテンプレートの性質 だからです。

pw.Go、タイムアウト、そして確定済みのレスポンスの中で境界が失敗したときの扱いは 非同期レンダリングにあります。

ライブレンダリング —— 誰も訊いていないとき

Section titled “ライブレンダリング —— 誰も訊いていないとき”

非同期レンダリングは遅い区画を1回届けます。チャットログ、メトリクスパネル、通知 フィードが欲しいのは逆です。サーバが何かを知り、いま誰かが見ているページの一部が それを言うべきだ、という状況です。

多くのアプリケーションでは代わりにポーリングを使います。その問題はリクエスト数 ではありません。そのほとんどが何も見つけないことです。

ポーリング ライブレンダリング 8回のリクエストのうち5回は空振り しかも更新は次の周期まで待たされた 1本の接続へ、何かあった瞬間に書き込まれる その間は何も流れない

ライブなソースは external live と宣言し、非同期の値と同じ {await} 節で束縛 します。だからソースを async から live に変えても、それを呼ぶテンプレートは 1つも変わりません。ハンドラに至っては何も変わらず、ブラウザは画面が開いている間、 ページ自身の URL に向けた接続を1本持ち続けます。

ソースのシグネチャ、再接続、購読がデータより長生きしたときの扱いは ライブレンダリングにあります。

部分更新 —— 2回目以降のすべての表示

Section titled “部分更新 —— 2回目以降のすべての表示”

描画されるチェーンのレイアウトとページは、すでに更新の境界です。識別子と描画結果の ダイジェストが生成時に書き込まれているので、古いダイジェストを携えて届いたリクエスト には差分で答えられます。

普通のコンポーネントは意図的に境界ではありません。この判断こそが、この仕組みを 安く保っています。

ドキュメントシェル — 差分は画面上のものを再利用するので、決して境界にならない layout.pw.html — 境界 サイトナビ 普通の コンポーネント search.pw.html — 境界、そして唯一変わったもの 500行の結果一覧 — コンポーネント1つであって500個の宛先ではない 送られるのは変わった境界のうち最も外側だけ。その上はリクエストが運んだダイジェストと一致し、その中身は一緒に付いてくる。 全コンポーネントを境界にすれば毎回のリクエストに500件が載る。普通のコンポーネントが境界でないのはそのためだ。

ランタイムは同一オリジンのリンクと GET フォームを横取りしてページ自身の URL を 要求し直し、返ってきたものを当てます。だから検索フォームは、アプリケーション側の JavaScript を1行も書かずに、いま開いているページを絞り込みます。どちら側にも書く コードはありません。enabled = true だけで手に入るのがこれです。

残り2つの経路 —— コンポーネント1つの再描画と、変更に対してその領域を返す応答 —— と、有効化に必須の validator key は部分更新 にあります。

ここで3つは本当に違います。「うまく劣化します」で丸めると、誰かの半日が飛びます。

非同期レンダリング ライブレンダリング 部分更新 確定済みのドキュメント、同じパスで 1ページ目の1往復と、progressive delivery を手放す 本物のスナップショット、そこで止まる スクリプト無しでサーバから押し込む手段は無いので、失われるのは更新のほう 失うものは何も無い リンクは辿れ、GET フォームは送信でき、戻るも効く — 最初からあった道だ

部分更新だけが例外で、その理由は注意深さではなく構造です。維持すべき フォールバック実装が存在しません。ランタイムは何も置き換えていないからです。 リンクは <a href> であり、絞り込みは <form method="get"> であって、ランタイムは そのマークアップがもともと自力でやることの最適化にすぎません。更新ヘッダを持たない リクエストには普通のドキュメントが返るので、クローラも curl もスクリプトを切った ブラウザも影響を受けません。横取りする側もこの基準で測られます。横取りした操作は すべて、ブラウザが自力で辿り着いたはずの行き先に辿り着かなければなりません。

非同期レンダリングは訊く必要があります。スクリプトを切ったブラウザはクローラでは なく、そうと分かる情報を何も送ってこないからです。スクリプトが無効なときにだけ動く HTML 機能は <noscript> ひとつなので、ストリーミングするページの head にブロックを 寄与し、同じページをマーカー付きクエリで指してリダイレクトさせます。マーカー付きの リクエストはバッファ経路で描画されます。読み手は求めたページに、完全な形で、同じ パスで着きます。

手放すのは progressive delivery と、訪問の1ページ目の1往復だけです。cookie が残りの ページ分の答えを覚え、スクリプトが動くブラウザはこの一切を見ません。scriptless_detection = false で切れますし、streaming = false という以前からのサイト全体の答えもあります(そちらは 全員の早期描画を手放します)。

救えないのはライブレンダリングのほうで、救えるふりをすべきでもありません。訊かれずに 配ることが機能そのものだからです。ただしゼロでもありません。バッファ経路は live 境界を 最初に配信された値で描画するので、スクリプトを切った読み手にも、解決しない フォールバックではなく本物のスナップショットが出ます。

どの機能も使わないほうがよい場面

Section titled “どの機能も使わないほうがよい場面”

100ミリ秒でリロードが終わるページには、どれも要りません。それぞれが本物の代償を 伴います —— ドキュメントごとのブラウザランタイム、配布する秘密、再描画は副作用を 持ってはいけないという規則、開いた画面ごとの接続。誰も気づかないリロードを消すため に払う額ではありません。

非同期レンダリングが適するのは、1つの区画がページの他の部分より明確に遅いとき であって、ページ全体が遅いときではありません。全部が遅いなら、直すのはクエリです。 部分更新は、リロードそのものが苦情の対象になるほど頻繁に更新される画面から。ライブ レンダリングは、代替が「ほとんど空振りするポーリング」になるときです。

そして入れ替えをアプリケーション側で持ちたいなら、どれも正しい形ではありません。 フラグメントとアイランドがテンプレートを1つ 描き、ネゴシエーションも順序保証も無しに、選んだライブラリへ渡します。