Catching up with SSR in a CSR environment

pjfb·2023년 2월 8일

この記事は、私もproduction環境で使ったことがないPoCレベルの内容をカバーしています。

ブラウザの動作原理を知っていますか?
Web Browserに住所を入力してEnterを押すとどうなりますか?

それで、私たちの最初のJavaScriptコードはEnterを押して数秒後に実行されるかもしれませんか?

私のアプリの最初のネットワークリクエストはいつ実行されますか?

これを学ぶために、CRAでアプリを作成し、非常に簡単なリアクトコードを書いてみました。
(この記事で知りたいことは、正確に言うと、「最初のJavaScriptコードが数秒後に実行されるのではなく」ではなく、「最初のネットワーク要求が何秒後に実行されるのか」です。)

const Product = ({id}) => {
   const [data, setData] = React.useState();

   React.useEffect(() => {
     (async() => {
       setData(
         await fetch(
           `https://63da01a2b28a3148f67cef79.mockapi.io/products/${id}/`
         )
       );
     })();
   }, [id]);

   return(
     <div>
       <div>ProductName: {data?.name}</div>
       <img src={data?.avatar} style={{ width: "100px", height: "100px" }} />
     </div>
   );
};
const App = () => {
   return(
     <Product id={3} />
   );
};

このコードを実行すると、一般的なCRAアプリで最初のネットワーク要求がいつ発生するかを確認できます。

ここでは0.4秒後ですね。

APIの応答には約0.2秒かかりました。

そうすれば、0.6秒(0.4 + 0.2)の後に(それも非常に高速なインターネット環境の場合)ユーザーにコンテンツを表示できるようになります。

もちろん、この部分についてもっと詳しく知っている人は、0.6秒も終わりではないことを知っています。 なぜならイメージは0.6秒からロードを開始するので、本当にロードが終わる時点はもっと長くなることもあるということです。

0.6秒は遅い時間ではありませんが、パフォーマンスは早ければ早いほど良いです。
_この記事で扱う内容は目で体感することができます。

記事の最後にお見せする予定ですが、SSRはどれくらい速いのか、CSRはどれくらい遅いのか、私が今回紹介するSSRとCSRの中間点を作ればどれだけ追いつくことができるかを映像でお見せします。

なぜ0.4秒待つのですか?

明らかにすべきことは、すべてのJavaScriptプロジェクトが0.4秒後に消費されるわけではありません。

プロジェクトの規模、あるいはネットワーク状況によって千差万別だろうが、私のテスト環境はlocalhostにCRAの基本構成なので、ほとんどの場合にこれより増えることはあっても減らすのは容易ではないかもしれません。

この部分を理解するためには、本当にブラウザのアドレスウィンドウでEnterを押すと何が起こるのかを知る必要があります。

もちろんここではDNSとかTCPとかこんな部分は扱いません。

*ブラウザは xxx.htmlページをロードします。 これはほとんどの場合非常に高速です。

  • HTML内の <script>タグがある場合はそれをロードします。 CRA環境なら、下の写真のbundle.jsです。 (名前は異なる場合があります。)

----ここまで0.16秒かかりました。 ------
ブラウザは実際にindex.htmlのDOMコンテンツをレンダリングします。 (下の写真の青い線)
青い線から赤い線までは何をするのか正確にはわかりませんが、どうやらJavaScriptエンジンを初期化して解析するのにかかる時間のようです。

----ここまで0.4秒かかりました。

*以降はJavaScriptが実行され、私たちが書いたコードが実行されます。

要約すると:

データを表示するにはAPIコールを実行する必要がありますが、APIコールはbundle.jsをロードする必要があります。
bundle.jsは、index.htmlがロードされるまでロードできます。

APIリクエストを始める前にも、複数段階のwaterfallがすでにできてしまっており、0.4秒をただ待つしかありませんね。

bundle.jsよりも早くJavaScriptを実行できますか?

もちろん可能です!

HTMLには scriptタグを書いてスクリプトを書くことができます。

<body>hi</script>
<script>console.log('hello world');</script>

index.htmlにスクリプトを直接書く方法は昔は一般的な方法でしたが、さまざまなフレームワークとバンドリングツールが進化し、今はタブー視されることになりました。 私はほぼ数年間 index.htmlscript タグを付けなかったようです。

私たちはしばらく過去に戻って、私たちのコードを最も速く実行できる index.html にコードを書きます。

<script>
prefetchStorage = {};
  
const prefetch=(url)=>{
   const task = async() => {
     const json = awaitawait fetch(url))。json();
     return json;
   };
   prefetchStorage[url] = task();
};
  
const sp = new URLSearchParams(window.location.search);
const id = sp.get("product_id");
  
if(id){
prefetch(`https://63da01a2b28a3148f67cef79.mockapi.io/products/${id}/`);
}
</script>

リアクトが初期化されていなくても、あるいは react-router-dom を書かなくても既に window.location にはアドレスを識別できる機能があります。

これを使用して、そのアドレスで**どのAPIを使用するかを事前に予測してプリフェッチするコードをハードコーディングすることができます。

const myfetch = async(url) => {
   if (prefetchStorage[url]) {
     console.log("cache hit: " + url);
     return await prefetchStorage [url];
   }else{
     const json = awaitawait fetch(url))。json();
     return json;
   }
};

アプリ内部の fetch 関数はこのように一度ラップしました。 実際のネットワーク要求を実行する前にprefetch poolにデータがある場合は、そのPromiseを代わりに返します。

本当に減ったのでしょうか?

(before)

(after)

今、私たちのアプリは bundle.jsがロードされる前にAPIリクエストを実行します!
そして、そのリクエストをreactからそのまま引き継いで処理できるようになりました。

index.htmlより速く実行できますか?

これも可能です。
それでもどんな方法があるのか説明してみましょう。

  • SSGは、なんと要求が入る前にJavaScriptを実行します。 ベストケースであれば、ユーザーはただCDNから単一のhtmlファイルをダウンロードするのに十分な待ち時間を必要とするかもしれません。
    *(SSGではなくても)SSRはこの部分で構造上より良い速度を提供します。

CSRとSSRの比較

ping 100msのLTE環境だとしましょう。

(50ms)と書かれた部分は、モバイルデバイスからモバイルデバイスへの一方向転送
(10ms)と書かれた部分は、サーバーとサーバー間の有線環境での一方向転送を意味します。

  • CSR: GET /index.html (50ms) -> RESPONSE /index.html (50ms) -> GET /bundle.js (50ms) -> RESPONSE /bundle.js (50ms) -> GET /products/3 (50ms) -> RESPONSE /products/3 (50ms) -> GET /imgs/product3.png (50ms) -> RESPONSE /imgs/products3. png(50ms)=400ms

  • SSR: GET /idnex.html (50ms) -> GET /products/3 (10ms) -> RESPONSE /products/3 (10ms) - > RESPONSE /index.html (50ms) -> GET /imgs/product3.png (50ms) -> RESPONSE /imgs/products3.png (50ms)=220ms

ここでは、2つの違いを見つけることができます。

1つ目は bundle.jsを最初にロードしなくてもページを描くことができるということで(ここで100msが減ります)
2つ目は、APIリクエストがサーバーとサーバー間で行われるため、信頼性が高く高速な有線LANで発生することです。 (50ms vs 10ms)
ここで80msが減ります。

そしてもっと速くしたい動きがあります。

これら2つのライブラリは、スピードのために非常に基本的な部分から再び考慮されたフレームワークです。

私も使ったことがないので上の2つについて詳しく書くことはできませんが、興味があれば読んでみてもいいようです。

だから何がどれくらい速いのですか?

最後に、私は簡単なテストコードを作成して実行した結果をアップロードします。

上から順に

  • SSR(SSG)
    *厳密に言えば、SSGではありません。 最初の行は index.htmlハードコーディングされた div と img です。 言い換えれば、これはSSGがやっていることを手で直接したものと見なすこともできます。
    • きちんとした SSG と私が CDN にデプロイした index.html はインフラ的違い、そしてパイプライン上の違いがあるかもしれませんが、この記事のテーマは TTFB(Time to First Byte) ではないことを考慮してください。
  • prefetched CSR
    • この記事で少ない手法を使って index.html から読み込みを開始した場合です。
  • CSR
    • 一般的な useEffect で fetch するコードです。

知っておくべきこと

この方法は非常に実験的であり、改善すべき部分がたくさんあります。 (例えばprefetchが失敗したらどうしようか)

私はこの記事で「実験的なprefetch」ではなく、CSRとSSRの根本的な速度の違いはどこから出てくるのかを知ることができれば良いようです。
(何がどのように戻るのかを知ることが早くする第一歩です)

0개의 댓글