TT Lab
はじめる
学ぶ 学習パス コース

SSR — サーバが先に描く

消えた時間はレンダーではなく往復にある

TT Labで続きを見る

一言でいうと

サーバーレンダリングが速くても、データを呼び出す順序が間違っていると、最初の画面のあとに往復が数珠つなぎに続きます。境界をどこに置くかが、その順序を決めます。

なぜ必要なのか

SSRを入れたのに体感があまり良くならなかったという話はよくあります。サーバーのレンダリング時間を測ると30msなのに、ユーザーが実際に使えるようになるまでに2秒かかります。消えた時間はレンダリングではなく、往復にあります。

典型的な形はこうです。ページがユーザー情報を受け取り、その中のチームidでチームを受け取り、そのチームのプロジェクト一覧を受け取り、各プロジェクトの最近の活動を受け取ります。コードで見れば自然な入れ子ですが、ネットワークで見れば1列に並んだ4回の往復です。レイテンシが200msなら、800msがそのまま消えます。

さらに悪い形もあります。コンポーネントごとに自分のデータを自分で受け取ると、親がレンダリングされないと子が存在せず、子が存在しないとそのリクエストが出ません。コンポーネントツリーの深さがそのまま往復回数になる構造です。

どう動くのか

お互いを待つ理由がないものはまとめて送る

ウォーターフォールをなくす最初の一歩は、「このリクエストは前のレスポンスの値を本当に使うのか」と問うことです。ほとんどは使いません。チーム情報と通知一覧はお互いを知らなくてよいのに、コードの形のせいで列に並んでいただけです。そういうものは一度に送り出せば、往復が1つに減ります。

本当に前のレスポンスが必要な場合は、リクエストをまとめる方向に進みます。サーバーで一度に組み立てて送れば、クライアントの往復は1回で、サーバー内部のクエリは同じデータセンターの中で行われます。

何を待つかは境界が決める

Reactのストリーミングレンダリングでは、<Suspense>境界の外にある部分をシェル(shell)と呼びます。シェルは先に送り出され、境界の内側は準備ができ次第、続けて流れていきます(renderToPipeableStream)。そのため、遅いデータを境界の内側に入れることが設計のすべてです。遅いもの1つがシェルに残っていると、その1つのせいで最初のバイトがまるごと遅れます。

注意すべきことが1つあります。境界を有効にするのは、境界を有効にするデータソースを読んだときだけです。Reactのドキュメントは、useで読んだPromiseなどを例に挙げながら、エフェクトやイベントハンドラーの中で受け取ったデータはレンダリングを止めないと明言しています(Suspense)。境界で囲んでおいてデータをエフェクトで受け取ると、フォールバックが表示されず、最初の画面には空の場所が描かれたままHTMLが出力されます。そしてその空の場所が、ハイドレーションの不一致の材料になります。

境界を細かく分けるほど最初の画面は速くなりますが、画面が何度もがたつきます。境界を大きく取ると静かな代わりに遅くなります。ここでの基準は好みではなく、その場所がユーザーが見てすぐ使うものかどうかです。

キャッシュとパーソナライズは同じレスポンスに同居できない

データをあらかじめ埋めたHTMLは、それ自体がそのユーザーのデータです。ssr-coreで見たように、パーソナライズされたレスポンスに共有キャッシュの指示を付けると、CDNが他人のページを渡します。しかし問題はレスポンスヘッダーだけではありません。同じURLが条件によって違う内容を出すなら、その条件をキャッシュが知っている必要があります。その役割を担うのがVaryです。メソッドとURL以外に、リクエストのどの部分がレスポンスの内容に影響したかを記すヘッダーで、Vary: *は、リクエストヘッダー以外の要因が関与したという意味なので、キャッシュできないことを含意します(Vary)。

実務で使える分け方はこうです。共有でキャッシュできるシェルを先に送り、人ごとに違う断片は境界の内側で別に受け取ります。そうすればキャッシュはシェルにだけ効き、パーソナライズは境界の内側に閉じ込められます。ハイドレーションの不一致を閉じ込める境界と同じ線です。

現場での姿

「サーバーレンダリングは30msなのに画面は2秒」という報告の大半は、ウォーターフォールです。確認する方法はプロファイラーではなく、ネットワークタブのウォーターフォール図です。リクエストのバーが階段のようにずれていれば列に並んでいて、並んで始まっていれば並列です。階段が何段あるかが、そのままなくせる往復の数です。

境界で囲んでおいて、データはエフェクトで受け取るコードもよく見ます。画面にはスピナーがきちんと表示されるので、正しく動いているように感じますが、そのスピナーはストリーミングのフォールバックではなく、クライアントの状態です。サーバーが送ったHTMLには空の場所しかなく、最初の画面の値は、やはりブラウザーが受け取ったあとでようやく生まれます。

最後に、ログインの有無で違うHTMLを出しているのにVaryを書いていないページは、CDNの前でいつか必ず事故を起こします。キャッシュが最初に受け取ったほうを全員に渡すからです。

境界を選ぶことは、結局1文に絞られます。ユーザーが見てすぐ使うものはシェルに、少し待ってもよいものは境界の内側に。その線を引くと、最初のバイトが何に縛られるか、どこまで共有でキャッシュできるか、不一致がどこまで広がるかが、まとめて決まります。3つが同じ線の上にあるということは、SSR設計で最も遅く気づく事実です。

次のクイズで確認すること

ウォーターフォールを見分ける場所、シェルとは何か、境界を有効にするものは何か、パーソナライズされたレスポンスにVaryがなぜ必要かを、6問で確認します。