![見出し画像](https://assets.st-note.com/production/uploads/images/170069238/rectangle_large_type_2_c062bdbd63899811d9d9e7510f4b08e8.jpeg?width=1200)
ウェブサイトのレンダリングの仕組みとその変遷に関する包括的な解説
23,487 文字
ウェブアプリケーションをレンダリングする方法は多岐にわたります。かつてはユーザーにHTMLを送信するだけでしたが、その後はユーザーにJavaScriptを送信して全ての処理をクライアント側で行うようになり、現在では様々な手法が混在する複雑な時代となっています。それぞれに利点と欠点があり、理解すべき要素が多くあります。私は全ての要素を図解するのに相当な時間を費やしました。これまでの中でも最も丁寧な説明をしようと努めた動画の一つだと思います。
この内容は、当初はran carniatoによる2025年のJavaScriptフレームワークの変化に関する素晴らしい記事を解説する予定でした。その記事は近日公開される予定なので注目してください。編集や構成が少し変則的に感じるかもしれませんが、途中で気づいたのは、このコンテンツ自体が単独で価値のある内容だということでした。
異なるタイプのレンダリング方式の仕組みと、それらがAstro、Next、Nuxt、Solidなどのフレームワークにどのように影響するかをより深く理解したい方には、この動画は非常に貴重な機会となるでしょう。様々なウェブ技術の基本的な仕組みについて、実際に多くのことを学べる珍しい動画の一つになっています。最後まで視聴できれば、これら4つの要素について理解できるはずです。
しかしまず、本日のスポンサーであるClerkからのメッセージをお届けします。Clerkは単なる素晴らしいチャートを提供するだけでなく、最高の認証プラットフォームを提供しています。これは、認証プラットフォームに懐疑的だった私からの言葉です。
当初、私は認証プラットフォームを必要としているとは思っていませんでした。自前で認証を実装するのはそれほど難しくないと考えていたからです。しかし、実際にメンテナンスを始めると問題が発生し始めます。コンポーネントを構築し始めると面倒になり、Googleのロゴの色やサイズに関する厳密なルールへの準拠、各プロバイダーのポリシー変更への対応、リフレッシュトークンの管理など、自前で実装した認証プラットフォームの管理は地獄のような経験でした。
React Native用の認証が必要だったのでClerkを試してみたところ、すぐに気に入りました。過去3年間にリリースした全ての製品で、サインインボタンがある場合は必ずClerkを使用しています。まさかこうして言うことになるとは思っていませんでしたが、私だけでなく、多くの人がそう感じています。Vercelの CEO、BasehubのCEO、そして私のような変わり者まで、みんなが支持しています。面白いことに、Supabaseの Paul Copplestoneも支持しています。Supabaseには独自の認証機能が組み込まれているにもかかわらず、SSO、MFA、その他の規制関連の要件が必要な場合には、Clerkが価値があると考えているのです。
コンポーネントの提供方法も素晴らしく、デフォルトで見栄えの良いサインインコンポーネントやユーザープロフィールコンポーネントを単にマウントするだけで使えます。デフォルトでの見た目が素晴らしく、十分なカスタマイズも可能です。実際、私たちはupload thingを構築する際に、Clerkのコンポーネントに大きく影響を受け、同様のアプローチを採用しました。
アプリケーションに認証を設定する際は、Clerkを試してみることをお勧めします。最初の10,000アクティブユーザーまで無料で、初日に解約した場合はカウントされないという寛容な条件です。私たちの経験は非常に良好で、皆さんも同じように感じられると思います。本日の動画のスポンサーとなってくれたClerkに感謝します。soy.lclarkで今すぐチェックしてください。
さて、ここに示されている2つの要素について説明しましょう。SPAの影響を受けたアイソモーフィックと、MPAの影響を受けた分割実行です。これらを理解することで、フレームワークの現状と今後の方向性がよりよく理解できるでしょう。
まず、それぞれの方向性についてさらに詳しく見ていく必要があります。左側に完全なSPA、右側に従来のMPAを配置してみましょう。従来のMPAは円を使用します。ユーザーがサーバーにアクセスすると...縦線を引いて場所を示した方が分かりやすいでしょう。ユーザー側とサーバー側があります。
ユーザーがウェブページにアクセスする、例えばexample.comにアクセスすると、サーバーは何かを返す必要があります。何を返すかが、ここでの重要な違いとなります。従来の伝統的なMPAでは、サーバーはページの完全なHTMLを返します。
ユーザーが別のページに移動したい場合、例えばexample.com/aboutに行きたい場合、サーバーは/aboutページの完全なHTMLを返します。これがMPA(マルチページアプリケーション)と呼ばれる理由は、ユーザーがナビゲーションをしてリンクをクリックすると、サーバーに対して「これが欲しい」というリクエストを送り、サーバーがそれに応答するためです。
これは文字通り、サーバーが各ページごとにファイルを持っているという意味ではありません。index.htmlやabout.htmlなどのファイルを持っているわけではありませんが、そのように考えることもできます。私がサーバーにアクセスすると、最後にスラッシュがあるだけの場合、サーバーはそれがインデックスを必要としていることを認識し、このindex.htmlファイルを取得してユーザーに送り返します。aboutページに行く場合も同様で、aboutページを見つけてユーザーに結果を返します。
しかし、サイトが動的である必要がある場合はどうでしょうか?HTMLにユーザー名などを含める必要がある場合はどうでしょうか?その場合は静的ファイルを持つのではなく、代わりにgenerateHomepageのような関数を持つことになります。この関数がユーザーに送信する実際のHTMLを生成します。aboutページに行く場合は、generateAboutPageが実行されるかもしれません。
この関数はリクエストを受け取り、必要な処理を行います。リクエストオブジェクトを使用してユーザーを特定し、適切なデータをHTMLに埋め込むことができます。重要なのは、HTMLの作成が、サーバー上で事前に行われるか、リクエスト時に行われるかのどちらかだということです。
ホームページは静的なindex.htmlなので即座に応答できますが、他のページは生成が必要かもしれません。要するに、サーバーはページを持っているか、ページを生成する必要があり、ページ全体を一度に処理する必要があります。
これを従来のSPAと比較してみましょう。ここで一つ補足する前に、赤い線を引きましょう。これはユーザーがリンクをクリックしたことを示します。つまり、この時点で図は効果的に完了しています。ユーザーが何か他の操作をするまで、このページは何もしません。追加のトラフィックは発生しません。ユーザーがサーバーにアクセスし、HTMLを取得し、ページを見ることができる、それだけです。
従来のSPAは少し異なります。SPAでは、index.HTMLを持っていますが、そのindex.HTMLには全てが含まれているわけではありません。非常に最小限の骨組みだけを持っています。通常、以下のようなものです:
htmlCopy<head>
<script src="app.js"></script>
<title>example.com</title>
</head>
<body>
JavaScript is disabled, please fix
</body>
従来のSPAを採用しているウェブサイトにアクセスしてHTMLを確認すると、このようになっています。私は現在Twitchでライブストリーミングをしていますが、ストリームページに行って、ネットワークタブでJavaScriptを無効にしてページをリロードすると、これが全てです。これがJSがロードされるまでのTwitchサイトの全てです。
このHTMLをコピーして、どれだけシンプルかお見せできます。実際に必要のない巨大なスクリプトタグを削除しました。ダークモード設定用のインラインスクリプトもありますが、それも削除します。全てのスクリプトタグを削除すると、Twitchの全HTMLは70行程度のコードになります。これが、JSを除いたTwitchが提供する100%のHTMLです。
では、何が起こっているのでしょうか?私がTwitchにアクセスしても、このように見えないのはなぜでしょうか?スクリプトタグが、実際の作業を行うさらなるJavaScriptを取得するようブラウザに指示します。ページ自体にはほとんど何もありません。
面白いことに、私がTheoのチャンネルページにいるにもかかわらず、タイトルは単に「Twitch」となっています。これは、どこかにリンクを投稿した場合に、メタデータを見つけるために追加の作業が必要になることを意味します。メタデータが存在しないのです。OGタイトルはありますが、ページタイトルが単に「Twitch」のままなのは面白いですね。
要するに、Twitchが提供する実際のHTMLは、どのページにアクセスしても効果的に静的ファイルとなっています。twitch.tv、twitch.tv/theprimagen、t.tv/theoのどのURLにアクセスしても、同じHTMLファイルが返されます。これが重要なポイントです。シングルページアプリケーションの全てのページが同じページ、同じHTMLにリゾルブされます。だからこそシングルページアプリケーションと呼ばれるのです。
完全なHTMLを送信するのではなく、スケルトンHTMLを送信します。このスケルトンHTMLは、body内に文字通り何もない場合もあれば、ローディング状態を持っている場合もあり、JavaScriptが無効な場合に何が起こっているかを説明するnoscriptタグを持っている場合もあります。重要なのは、このscriptタグを持っているということです。
レスポンスがユーザーに届くと、別のリクエストを行う必要があります。JavaScriptを取得する必要があります。ここでインライン矢印を使用しましょう。twitch.tv/theoにアクセスすると、基本的に空の静的HTMLがJSタグと共に返され、ブラウザはそのJavaScriptスクリプトを取得する必要があります。
JSを受信したら、ブラウザは何かをする必要があります。そのJavaScriptを処理する必要があります。これらの線をもっと長くする必要がありますね。ここで、シングルページアプリケーションが多くのケースで適切なツールではない理由が分かり始めるでしょう。
ページはJSをパースして実行する必要があり、api.twitch.tvにアクセスして情報を取得する必要があります。このAPIは、Twitchの他のクレイジーなサービスにアクセスします。この場合はGraphQL Edgeです。このGraphQL Edgeは他にも多くのことを行いますが、それについては触れる必要はありません。どのようなスタックでも同じようなことができます。
重要なのは、そこにアクセスしてJsonレスポンスを取得するということです。ユーザーはこれを要求し、ある程度の時間が経過した後、Jsonが返されます。少し上に移動させて、そんなに酷く見えないようにしましょう。これがユーザーに返されますが、HTMLではなくJsonブロブとして返されます。
これでクライアントは理論的に必要なものを全て持っているので、クライアントはそのJsonを使用して適切なHTMLを作成し、最終的にページがロードされます。
これを見て「おいTheo、誇張しすぎだろう。そんなに多くのステップがあるはずがない」と思うかもしれません。確かに誇張していますが、逆方向に誇張しています。実際には、一つのAPIコールだけではありません。一つのJSファイルを取得するだけでもありません。
JSファイルを取得すると、URLを確認し、そのURLにはさらに多くのJSが必要だと判断します。そして、さらにJavaScriptを取得します。必要なJS全てを持っている場合にようやく、APIからデータを取得するなどの必要な作業を開始できます。
これも一度だけ行うわけではありません。おそらく複数のコールを行うでしょう。再びTwitchを例に取り、JavaScriptを有効にして、ネットワークタブを見てみましょう。クリアして...速すぎてクリアもできないほどです。クリアしてリロードします。
ストリームを一時停止しても、これだけの量のデータが入ってくるのが分かります。これがTwitchをロードする際に発生することを理解していますか?XHRリクエスト(エンドポイントにヒットするもの)だけに限定しても、これら全てのAPIコールがページの表示が完了する前に行われています。文字通り数十のリクエストがユーザーに何かを表示する前に行われなければならないのです。
これが現在のウェブの仕組みの一部であり、人々はこれがReactの問題だと理解していないようです。Reactというフレームワークに根本的な欠陥があるわけではなく、クライアントで全てを行う能力があるため、そうしてしまう傾向があるのです。
これには多くの理由で問題があります。ここまでのステップを経てようやくナビゲーションができるということが分かっていただけたと思います。ユーザーがリンクをクリックすると、シングルページアプリケーションは少し良くなり始めます。なぜなら、新しいUIの表示を開始するためにどこかに行く必要がないからです。
ブラウザのラインをもう一つ追加すべきでした。ここで面倒ですがそうしましょう。これをbrowに変更し、userの新しいラインを作ります。ユーザーがtwitch.tv/theoと入力すると、ブラウザがサーバーに行き、空のHTMLを取得し、JSを取得し、JSをダウンロードし、JSをパースして実行し、APIコールを行い、Jsonを取り戻し、Jsonブロブを取得し、それがレンダリングされます。
ここで違う描き方をすべきだったのは、Jsonがユーザーへのページの最終表示につながるHTMLの作成に使用される部分です。そして、ユーザーがリンクをクリックします。
クールなのは、以前に同じ図を上で描いた場合、ブラウザは何かを取得するためにサーバーに行く必要がありましたが、実際にはこのギャップは同じように描いていますが、このギャップの方がはるかに小さく、サーバーの方がはるかに遠いということです。
何かを行うためにサーバーに行く必要がない場合、それは良い感じです。全てをローカルに持っていることには多くの利点があります。リンクをクリックすると、ユーザーは/theprimeagenにナビゲートしようとしています。これがマルチページアプリケーションで構築されていた場合、適切なHTMLを見つけるか生成するためにサーバーを待つ必要があり、それが戻ってくるまで何も表示されません。
その間、Chromeの上部に青いローディングバーがゆっくりと進んでいくのを見ることになり、サーバーが完了するまで何かをクリックしたというフィードバックすら得られません。
しかし、シングルページアプリケーションでは異なります。/theprimeagenに移動すると、ブラウザは即座に新しいページを生成します。実際の新しいページではありません。HTMLを変更します。なぜなら、シングルページアプリケーションでは何も変わっていないからです。サーバーから送信されたHTMLは同じままです。
URLに基づいて新しいページを生成します。React Routerを使用している場合、特定のURLが特定のUIの部分に紐付けられており、これによってURLを変更したときにどのコンポーネントをレンダリングするかを知ることができます。それらのコンポーネントのレンダリングを開始し、新しいUIが表示されますが、それらのコンポーネントの一つにデータが必要です。
そのため、これを生成した後、さらにデータが必要だと気づきます。/theprimeagenのための新しいデータをリクエストします。おそらくapi.twitchになるでしょう。api.twitch.tv/get/theprimeagenとしましょう。ここでもう一度Twitchサーバーにアクセスしてデータを取得します。
この部分をコピーペーストしましょう。全く同じです。Jsonを返し、そのJsonがブラウザに届き、ブラウザはそのJsonを使用して正しい状態を生成します。それまでは、おそらくローディングスピナーなどを表示することになります。そのJsonを取得すると、ユーザーに正しいコンテンツを提供するための楽しい機能が全て利用可能になります。
クールなのは、即座に何かが表示されるということです。ユーザーは何かを見ることができ、必要なデータがキャッシュされている可能性もあり、そのページをクリックすると全てが即座にロードされます。これは本当にクールですが、全てのケースに適しているわけではありません。全ての人に適しているわけではありません。
従来のMPAと比較するために、ここで同じ内容を追加します。ユーザーがリンクにアクセスすると、example.comのサーバーがHTMLを送信し、ユーザーは結果を見ることができます。素晴らしい、楽しいですね。
しかし、ユーザーがリンクをクリックすると、考慮すべきは、ブラウザとサーバーの間のこの深淵を越えるたびに待つ必要があるということです。example.comにアクセスすると、全てを取り戻すまでしばらく待つ必要があります。
リクエストを送信してHTMLを取り戻しますが、上部のローディングバー以外に何かが進行しているという表示はありません。この時間、例えばTwitchの場合、twitch.tv/theoからtwitch.tv/theprimeagenに移動したとき、リンクをクリックした後もサーバーが完了するまではTheoのストリームを見続けることになります。サーバーがそれらの処理を完了するまでページは変更されません。
サーバーがこれらの処理に時間がかかる場合、問題が発生します。これには多くの落とし穴があり、ここから新しいモデルの話に入っていきます。後でそれらについて説明しますが、まず、ここについて話す必要があります。
私の意見では、最大の問題はこの上部の部分です。下部の部分は実際には良いものであり、従来のマルチページアプリケーションの方法よりも多くの点で優れていることに同意していただけると思います。
問題は、ユーザーがしばらくの間正しいコンテンツを見ることができず、実際にデータを取得する前にこれら全ての処理を行わなければならないということです。
ここで、Ryanが記事で言及した2つの重要な部分に戻ります。SPAの影響を受けたアイソモーフィックと、MPAの影響を受けた分割実行モデルです。
シングルページアプリケーションのこのカオスに対する最初の解決策は、考えてみれば実際にかなり直感的なものでした。SSR SPAと呼びましょう。
ここでは、空のHTMLファイルとJSタグを送信し、クライアントが全ての作業を行うのではなく、かなり異なることを行います。今度はサーバー上でReactを実行します。ReactのServer Runtimeです。
なぜサーバー上でReactを実行したいのでしょうか?実は非常にシンプルな理由があります。空のHTMLではなく、完全なHTMLが欲しいのです。これにより、ユーザーに即座にそれを返すことができます。
興味深いのは、ユーザーがそのHTMLを取得すると同時に、並行して、それにはやはりJSタグがあることを知っているので、そのJSタグを取得し、JSを受信し、JSをパースして実行できることです。APIコールが必要な場合はそれを行いますが、サーバーランタイムが実際にHTMLにデータを埋め込んでいる可能性が高いです。
例えば、Pingにアクセスしてみましょう。Pingはライブストリーマーがコラボレーションをより簡単に行えるようにするために私が作成したビデオコールサービスです。ここでNextの__NEXT_DATAを見ると、NextJSがサーバー上でページをレンダリングするために使用したプロパティがあり、それらはHTMLに埋め込まれているので、Reactコードが制御を引き継いだ後に再フェッチする必要はありません。
これはハイドレーションと呼ばれるものを行います。ハイドレーションとは、クライアント上にJavaScriptがあるけれども、HTMLも既に書かれている状態を指します。通常、ReactはHTMLを全て書き込みますが、HTMLが既に書かれている場合、Reactコードはどのボタンにonclickをトリガーするかをどのように知るのでしょうか?
それを解決するために、クライアント上で同じReactコードを再実行することでボタンを特定します。これが重要なポイントです。Reactコードは最初にサーバー上で実行されます。緑色でこれらにラベルを付けましょう。「First React Run」とします。
Reactは最初にサーバー上で実行されます。ビルド時に実行される場合もあれば、デプロイ時に一度だけ実行される場合もあります。動的に実行される場合もあり、ユーザーがリクエストを行うと、新しいHTMLを新しい埋め込みデータで生成します。
HTMLはこの実行に必要なデータを埋め込み、JSがそれを取得し、次にブラウザランド上で2回目のReact実行が行われます。つまり、サーバー上で一度実行され(ビルド時またはリクエスト時)、必要なHTMLとデータを埋め込み、JSをパースして実行すると、この部分をスキップできます。
サーバーがそれらを既に行っていることを前提とすると、これら全ての部分を省略できます。実際に起こっていることは、このReact実行中にサーバーがGraphQL EdgeのようなE外部サービスにアクセスするか、直接データベースにアクセスします。
サーバーがデータベースにアクセスし、Jsonを取得し、またはサーバーが返す何かを取得し、それをHTMLとしてデータを埋め込んでユーザーに送信します。ブラウザは必要なJavaScriptを取得する必要があり、それを受信すると、JSを実行し、必要な全てのデータとリソースにアクセスできます。
これがハイドレーションです。ハイドレーションとは、同じReactコードを再実行して、ReactがHTML内の正しい場所にリンクできるようにすることです。これで完了です。
では、デメリットは何でしょうか?何か悪いところがあるはずです。良すぎて真実とは思えないような感じがしますよね?
確かにデメリットがあります。デメリット1:ここでボタンをクリックすると何が起こるでしょうか?alertを持つonclickを持つボタンがあり、JSがロードされ、パースされ、ボタンにバインドされる前にクリックした場合、何が起こるでしょうか?
ボタンがどのonclickにバインドされているかが分かりません。Reactでbuttonのonclickをこのように書いた場合、実際のHTMLにはonclickはそのようには存在しません。Reactは仮想DOM内で全てを処理しているからです。
ボタンにはHTML内にonclick関数が埋め込まれておらず、JSが実行されブラウザによってパースされるまで、それを適切にアタッチする必要があります。これは問題です。なぜなら、ページがロードされて表示され、すぐにボタンをクリックしても、JavaScriptによってボタンがハイドレーションされるまで機能しないからです。
これが最大のデメリットです。ボタンはまだ機能しません。その後、これがロードされると全てが機能するようになります。これは非常に良いモデルで、多くの問題を解決しましたが、これが唯一のデメリットではありません。
また、明確にしておくべきことは、これには解決策があるということです。ほとんどの場合、このHTMLに非常に最小限のJSタグを送信し、ボタンをクリックしたときのイベントをすべてキューに入れ、JSがロードされたらそれらを再生するという方法を取ります。これはほとんどの場合うまく機能し、問題を解決します。
誰に聞くかによって意見は異なります。Qwikのような人々に聞けば、このハイドレーションは全ての悪の根源であり、それを破壊しようとしていると言うでしょう。私は彼らが過剰反応していると思いますが、まあ、私はReactブロなので、当然みんな過剰反応していると思うでしょう。
しかし、もう一つのデメリットがあります。このように構築する場合、送信されるデータ量が多くなります。データを複数回送信する必要があります。私の好きな例は、再びPingのプライバシーポリシーです。このモデルの別の問題を示すのに最適な例です。
ここにページのHTMLがあり、ネットワークタブに移動してハードリフレッシュすると、そのレスポンスを見ることができます。ここにそのページのHTMLがあり、プライバシーポリシーの全てのテキストがここにあることが分かります。シングルページアプリケーションの場合は空で、ページをロードし、JSをロードし、データをフェッチするか、またはJavaScriptに埋め込み、最後にコンテンツが表示されます。
では、問題は何でしょうか?理想的に見えますよね?お見せしましょう。このJSファイル、privacy-policy.[random name].jsを見てください。ここからランダムな部分を取り出してみましょう。「a particular service」で検索してみると...ご覧ください。ネストされた子P要素、children、strong、そしてここに全く同じテキストがあります。
このプライバシーポリシーのテキスト全体が2回送信されています。一度はHTMLとして送信され(正しいHTMLをユーザーに送信したため)、もう一度はJavaScriptとして送信されています(これはそのHTMLを作成した元のJavaScriptであり、同じJSをクライアントとサーバーで実行する必要があるため)。
これは、Reactが何がどこにあるかを知るために必要です。なぜなら、もしこの段落の中にボタンがあり(何らかの理由で)、そのボタンにonclickがあり、このテキストを全て持ってボタンをそこに配置し、onclickを配置するようにレンダリングしなければ、機能しないからです。
しかしこれは、データを二重に送信するだけでなく(HTMLとして送信し、全く同じコンテンツをJSとして送信する)、JSに私たちが実際には持ちたくない全てのものが含まれているということを意味します。プライバシーポリシーがクライアント上で更新可能かどうかは気にしません。それは重要ではありませんが、このコンテンツを全て2回送信しなければならず、それがJSのVMとランタイムに存在するのは良くありません。必要のないデータが増えるだけです。
また、これは検索エンジン最適化も損なわます。ブラウザのクローラーはJSをダウンロードして実行し、ページを生成することはしません。単にHTMLを見て、その中に何があるかを確認したいだけです。そのため、HTMLにコンテンツがあることには多くの利点がありますが、ページ上にいる間に変更されない大量のコンテンツがある場合、JSに属さない多くのものをロードしているだけで、これはバランスを取るのが難しい問題です。
SSRシングルページアプリケーションについて、もう一つ興味深い詳細があります。ユーザーが新しいリンクをクリックすると、クライアントは依然として新しいページを生成することができます。そのように設定すれば、即座に何かを表示し始めることができますが、サーバーにアクセスして何かを要求することもできます。
twitch.tv/theprimeに行くと、サーバーは全く新しいページを生成する代わりに(ここでしたように)、データ部分だけを取得します。ページを最初にロードする際には、Reactサーバーランタイムがユーザーが見るHTMLを生成する必要がありますが、それ以降は、特定のページのこのJsonブロブを取得するだけでよいのです。
そのJsonにはすべてのデータがあり、JavaScriptは既にクライアント上に存在するので、ページを正しくレンダリングするために必要なものはすべて揃っています。これは、両方の部分の利点の多くを得られることを意味します。即座に新しいページが表示される体験が得られ、それらのローディング状態を制御する能力があり、全く新しいHTMLページを待って全てを再レンダリングする代わりに、必要なJsonだけをフェッチすることができます。
もう一つのデメリットがあります。例えば、Twitchで動画を再生中にaboutページへのリンクをクリックした場合を考えてみましょう。MPAの場合、プレーヤーは存在しなくなりますが、シングルページアプリケーションの場合、ブラウザがナビゲートせず、ページを変更しないため、プレーヤーを下部の小さなピクチャーインピクチャーのようなポップアップとして表示し続けることができます。
これは従来のマルチページアプリケーションでは事実上不可能です。これを実現可能にするAPIの提案もありますが、ページの残りの部分が変更される間、要素を永続的に保持することは、ブラウザが得意とすることではありません。これは、シングルページアプリケーションが非常にクールな理由の一つであり、このモデルではそれらの利点を全て得ることができます。
正しいHTMLがユーザーに送信され、探索や他のことを行う際により良い即時ナビゲーションが得られ、Jsonブロブをフェッチして、その間にローディング状態を表示できるという、ユーザーに適切なコンテンツを提供するためのはるかに優れた体験が得られます。本当にクールな仕組みです。
残念なことに、Nextの動作方法は、そのJsonブロブを取得するまでナビゲーション時に全ページのロードをブロックします。従来のPagesディレクトリ(古いルーター、つまりapp/ではなくpages/)でNextを使用する場合、OG NextJSの従来のシングルページアプリケーションSSRの仕組みでは、ナビゲート時に新しいページをすぐには表示しません。サーバーがそのデータブロブを生成するのを待たなければならず、それが完了するまで待機状態になります。
少なくとも、ページ上の要素を永続的に保持できるので、利点と欠点がありますが、私が本当に望んでいた即時ナビゲーションは得られません。ユーザーに何か新しいものを即座に表示したかったのです。
そこで私がしなければならなかったのは、このサーバートリップを必要とするすべてのデータを、ページのロード前には行わないようにすることでした。PINGのダッシュボードに行くと、興味深いことが分かります。クリックした瞬間にローディング状態が表示されます。
これを実演しましょう。今クリックしています...即座にページが表示されました。今回はデータがキャッシュされていたのでより速くロードされましたが、キャッシュされていない場合は...ハードリフレッシュして、今クリックします...最初にそのローディング状態が表示されます。
このローディング状態が表示される理由は、ナビゲーションをブロックするデータを持ちたくないからです。クリックした時に即座に何かを表示し、その後でAPIから必要なデータをフェッチしたいのです。
このモデルは最初の正しいHTMLを生成するのにとても優れていますが、全てに使用すると、全てのナビゲーションが再び遅くなるというデメリットがあります。ある意味で、これは従来のシングルページアプリケーションとマルチページアプリケーションの両方の長所を備えていますが、同時に両方の短所も備えています。
クライアントに送信されるデータが多すぎ、シングルページアプリケーションのようにクライアントに過度に依存し、マルチページアプリケーションのようにナビゲーションをブロックしてしまいます。
これらの問題をどのように解決するのでしょうか?ついに、この長い脱線の後で、Ryanが記事で指摘した2つの重要なポイントに戻ってきました。シングルページアプリケーションの影響を受けたアイソモーフィックと、マルチページアプリケーションの影響を受けた分割実行です。
これらは2つの異なる戦略の進化形です。これは標準的なシングルページアプリケーションの進化形ではありません。なぜなら、SPAから完全なSSRに移行したからです。完全なSSRとは、私が先ほど示した、両方の場所で全く同じReactコードを実行するものです。
はい、良いですが、素晴らしいわけではありません。今や、アイソモーフィックSSRがあります。アイソモーフィックSSRとは、サーバーとクライアントで異なる挙動をするコードです。これは、ローディングラッパーやsuspenseラッパーのようなものを使用でき、サーバーが全てのHTMLを送信するのを待つ代わりに、即座に何かを表示できることを意味します。
このページ、HTMLにはbody、nav、div id="sidebar"、div id="content"があるとしましょう。これが私たちのHTMLです。これはtwitch.tv/theoのHTMLで、ID="content"があり、実際のコンテンツは「Theo」と表示されています。
もちろん、theprimeagenのような異なるページのHTMLは異なりますが、それほど違いはありません。それは良くありません。楽しくありません。このナビゲーション時にサーバーが完了するのを待たなければならないのは良くありません。navでリンクをクリックして別のページに移動する時、サーバーが完了するまで待つ必要があるのはなぜでしょうか?
(なぜdiv id="content"ではなくmainを使用しないのかという質問については、私が怠惰だからです。でも、要点は分かると思います)
これを少し変更してみましょう。今、これはHTMLですが、これがJSXだと想像してください。ここに何らかの指示を入れます。「これを待たないで」というような指示です。ここで終わりにしましょう。こちらでも同じことをして、その部分を何らかの形の指示でラップし、その部分を待ちたくないことを示します。
これで理論的には、ナビゲーション時に他の全てが同じであることが分かっているので、それらの部分を残し、ここで何かを行い、その後で追加のデータを取得することが可能です。さらに一歩進んで...
再びこれがJSXだと仮定しましょう。suspenseという素晴らしいコンポーネントがあります。この部分をsuspenseでラップし、ローディング状態またはfallback="loading"を与えると、クライアントがページを最初に取得した時、この部分のレンダリングを待たずに済みます。
素晴らしいですね。suspenseの仕組みが分かってきたと思います。suspenseで何かをラップすると、もはやレンダリングをブロックせず、より速くものを取得できます。
これが本当にクールになるのは、ナビゲーション時です。異なるページに移動し、これらの境界内にある要素だけが変更された場合、navのリンクをクリックしても、すぐにはtheprimeagenの内容は返ってきません。代わりに、ローディングの部分が表示されます。
クリックした瞬間に何かが表示され、サーバーは正しいコンテンツをそこに配置するために必要なものを取得します。さらにクールなのは、Jsonを送信してから正しい状態をレンダリングする代わりに、正しいHTMLを直接送信することができることです。
これを図解してみましょう。アイソモーフィックSPAです。実際には、ここの上半分はほとんど同じように扱うことができます。Reactのサーバーコンポーネントのように、サーバーでのみ実行されるコンポーネントがある場合、そのデータをJavaScriptの一部として送信する必要がなくなるという利点があります。
まあ、要素がどこにあるかを知る必要があり、奇妙な方法でシリアライズされるので、二重データの問題を完全に排除するわけではありませんが、少なくとも以前ほど悪くはありません。JSランタイムに同じ方法で存在するわけではありません。それは異なります。
しかし、ほとんどの部分で、アイソモーフィックモデルでは最初のページロードはほとんど変わっていないと言っても公平でしょう。アイソモーフィックの重要なポイントは、異なる場所で異なる実行をするということです。
同じコードの一部はサーバーでのみ実行され、一部はクライアントでのみ実行され、一部はサーバーとクライアントで異なる実行をします。ここに魔法が始まります。なぜなら、theprimeagenに移動する際、別のページに移動してコンテンツが異なる場合、ここで行ったようにサーバーを待つ代わりに、2つのことを同時に行うことができるからです。新しい状態を表示する代わりに、即座にローディング状態を表示できます。なぜなら、クライアント上で実行されているsuspense境界がブラウザとReactに「この新しいページに移動する時、残りのデータを待っている間はここにローディング状態を表示してください」と伝えるからです。
データリクエストがsuspense境界やなんらかのローディング表示の下にある限り、ブラウザがデータを取得するのを待ってからユーザーに何かを表示する必要はなくなります。そのため、サーバーが必要な要素を生成している間、即座にローディング状態を表示できます。
これで、以前はできなかったことができるようになりました。以前は、さらなるリクエストでReactサーバーランタイムを実行せず、単にJsonを生成するだけでしたが、もはやそうではありません。Reactの実行回数を考えることも不要になったので、その部分も削除しましょう。
今、このページにアクセスすると、サーバーランタイムが新しいページを生成し、新しいJsonをサーバーランタイムに送り返し、これで変更されたHTMLをブラウザに送信できます。ブラウザは新しいHTMLのチャンクを受け取り、それらを持っていれば、ローディング状態を置き換えることができます。
つまり、正しいコンテンツでローディング状態を置き換えることができます。これを説明する他の方法が分かりません。とにかく素晴らしいのです。
何かをクリックして即座に結果を見るというシングルページアプリケーションの利点と、Jsonを送信せずに異なるレンダリング方法を使用できる利点の両方を持てることは本当に素晴らしいです。
例えば、理論的に、誰のページに行くかによって異なるSVGがたくさんあり、PrimeとTheoで異なるSVGがあり、それらが大きく、何百何千もあるとしましょう。それが全てクライアント上のJavaScriptにエンコードされていた場合、UIが取りうる全ての状態に対応する巨大なJSファイルが必要になります。
従来のReactアプリケーションでページにHTMLがある場合、それをレンダリングするReactコードはそのJSバンドルに含まれていなければなりません。アイソモーフィックなシングルページアプリケーションのサーバーコンポーネントスタイルの開発では、異なる状態をレンダリングする大量のJavaScriptを持つという魔法のような能力を持つことができます。
全てのJSとJsonを送信してHTMLを作成する代わりに、その巨大なTypeScriptファイルをサーバーに置いておき、HTMLだけを送信することができます。UIが取りうる全ての状態を含む巨大なJSと、そのレンダリングに必要な実際のデータを含むJsonを送信する代わりに、HTMLだけを送信できます。
これはHTMXのような技術が魔法のように感じる理由でもあります。ブラウザにJsonを送信してHTMLに変換するのではなく、単に正しいHTMLを送信するだけです。これは本当にクールで、これらの全ての解決策の利点を実際に得始めています。SPAの即時ナビゲーション、MPAのHTML優先の開発、そしてそれら2つの間で戦っているような感覚のない開発者体験が得られます。
しかし、いつものように落とし穴があります。最大の落とし穴は、そのHTMLファイルを静的なCDNから取得できなくなることです。ここの最上部で見たような従来のSPAのように、Twitchの全てのページが同じindex.htmlファイルにリゾルブされる静的なHTMLファイルを持つことができません。
つまり、Twitchにアクセスした時のそのファイルをダウンロードする際のレイテンシーは瞬時で、待ち時間は全くありません。これは本当に本当にクールです。しかし、静的なHTMLを持つことができないということは、全てのリクエスト(ウェブスクレイパーであれ、従来のユーザーであれ、その間の全て)が、ページを生成し、必要なコンテンツを埋めるためにサーバーサイドのJavaScriptを実行しなければならないということです。
これを設定するのは簡単ではありません。Vercelのようなクールなプラットフォームがあり、Netlifyも同様のことを始めていると思いますが、彼らはReactサーバーランタイムに直接アクセスする代わりに、前面にエッジを配置します。
エッジワーカーと呼ばれるこの中間層は、アクセスしようとしているURLをチェックし、そのURLに対応するキャッシュされたHTMLファイルがあるかどうかを確認します。もしあれば、そのHTMLファイルを取得して送信し、なければサーバーランタイムに処理を戻して実際のページ生成を行い、それを返します。
部分的なプリレンダリングという追加のレイヤーもあり、最初の静的な部分とそれを完了するためのサーバーランタイムコードの両方を取得できるので、より速く何かを取得し、残りを後で取得することができます。これについては動画が長くなりすぎるので、部分的なプリレンダリングに関する私の他のコンテンツをチェックしてください。
ここでのポイントはシンプルです。このようなモデルでは、もはや静的なHTMLファイルを送信することはできません。バックエンドコードが必要で、単にJavaScriptを実行するだけでなく、Reactを実行する必要があります。
ウェブサイト全体と全てのURLをリゾルブするサーバー上でReactコードを実行していない場合、このモデルを使用することはできません。Twitchがこのモデルに移行する場合、静的なHTMLファイルを提供し、全てのエンドポイントをGoコードで実行することはできません。少なくともtwitch.tvサイト自体はJavaScriptによってリゾルブされる必要があります。
多くの人がこれを躊躇する理由が分かります。フロントエンドチームはフロントエンドチームであり、APIをスピンアップしたりそのようなことをする権限がない会社もあります。
そしてここで、この4番目の非常に魅力的なオプションが登場します。MPAの影響を受けた分割実行です。このモデルは実際に、先ほど説明したアイソモーフィックモデルと非常によく似ています。
これらの部分をもう一度取り上げましょう。suspenseを持つこのHTMLページがあり、それによってReactがローディングスピナーをどこに配置し、何を待つか、待たないかを知ることができると言いましたね。アイソモーフィックMPAの仕組みは少し異なります。
1つのリクエストでリゾルブし、準備ができたら動的なデータを送信したり、全体が完了するまでブロックしたりする代わりに、この要素はアイランドであり、このアイランドには何をする必要があるかの指示があります。elementがあるとしましょう。Island source="/content/theo"とします。
これは何をするのでしょうか?まず第一に、これを静的なCDNでホストすることができます。このファイルをS3に投げ込むだけで良いのです。これは本当にクールです。
これで、シングルページアプリケーションと従来の静的なマルチページアプリケーションの両方で慣れている方法でホストすることができます。S3のような静的なCDNに多くのHTMLファイルを置いておき、URLにアクセスすると即座にHTMLを提供します。
では、このアイランドは何をするのでしょうか?ここで話題のパターンが登場します。本当にクールなのですが、おそらくこれは同じ場所をsourceとしません。おそらくapi.example.comやastro.example.comなど、HTMLを送信するだけの静的なCDNではないことを示すものになります。
このアイランドは必要な新しいHTMLを取得するためのネットワークリクエストを行います。つまり、ページは実際のサーバーを使用せずに静的にロードでき、キャッシュすることができ、従来のHTMLから期待される全てのことができます。
単にページにアクセスしてロードし、動的な部分があればそれを取得します。このアイランドがTheoのコンテンツではなく、私のショッピングカートだとしたら、ショッピングカートの周りのページは全てのユーザーで同じかもしれませんが、そのショッピングカート部分は個人的なもので、私のためのものです。
全体のページをレンダリングするために全てのサーバーを待つ必要はありません。静的な部分があり、個々の部分が「これは動的で、これのJSを取得し、その後で実際のデータを取得する」と示されている場合、モデルを逆転させたことになります。
ここからアイランドという用語が生まれました。動的でインタラクティブなアイランドがあり、残りは全て静的です。この場合、twitch.tv...amazon.com/cartに変更しましょう。このアイランドまでは、全てのユーザーが全く同じページを見ます。異なるのはこのアイランド内の部分だけです。
Astroのようなものをこのサーバーサイドアイランド作成に使用する場合、ページがロードされると、非常に小さなJSが実行され、このURLにアクセスします。サーバーサイドで、あなたのクッキーなど全てを使用して、この部分に埋め込む正しいコンテンツを生成します。
図解してみましょう。ユーザーがamazon.com/cartにアクセスすると、Amazonはこの事前生成された静的なHTMLファイルをサーバーに用意しており、それにアクセスするのを待っているので、事実上即座にロードされます。
これは完全なHTMLではなく、ほぼ完全なHTMLです。ユーザーに送信され、これがロードされるとすぐに、非常に小さなJSを実行する必要があります。それを示すのはほとんど不公平に感じるほど小さいのですが、ここで示します。
埋め込みJavaScriptを実行し、この埋め込みJSはページを通過して、アイランドを探し、ここで描いたアイランドを見つけ、ショッピングカートのコンテンツを取得するためのソースが何であれ見つけます。
ここで、サーバーに再度アクセスして「/content/shopping-cartのHTMLが必要です。まだ準備ができていない空の部分に入れるHTMLが必要です」と言います。この時、ユーザーはアイランド以外の全てを見ることができます。
ユーザーはアイランド以外の全てを見ることができ、この埋め込みJSを実行し、ショッピングカートデータを持つこのエンドポイントにヒットし、ここで行った楽しいことを全て行います。これを取得しましょう。
Astroは好きなフレームワークをサポートしているので、React サーバーランタイムを実行することもできますが、実際にはこのようなサーバーアイランドを使用する場合、おそらくAstroを通じて行うでしょう。そのため、Astroサーバーランタイムと呼びましょう。
Astroサーバーランタイムはデータベースにアクセスし、Jsonを取り戻し、ここでアイランドのHTMLコンテンツを作成します。Astroサーバーは残りのHTMLコンテンツを作成し、それをほぼ直接送信できます。JSのやり取りは必要ありません。単にコンテンツをブラウザに送信するだけです。
残りのHTMLを送信し、ブラウザはその小さなJSを使用して、それがどこに配置されるべきかを理解し、アイランドに配置します。これで完了です。
この解決策の本当にクールな点は、最初のロード時に静的なCDNの利点を得るために変なことをする必要がないということです。
サーバーコンポーネントモデルに移行した時、つまりPingを従来のSPAから、新しいサーバーレンダー優先のメソッドを使用するupload thingに移行した時に受け入れた最も辛いことの一つは、HTMLが完全に静的で動的な動作が全くないか、完全に動的でユーザーが一つのものを見る前にサーバーを待たなければならないかのどちらかでなければならないということでした。
私はこれらの妥協の両方を嫌いました。そして、結局私がやっていたのは、巨大なローディングスピナーを持つ静的な、ほぼ完成したHTMLファイルを生成し、そこからクライアントで全てを行うということでした。
そのため、Pingは一度ページがロードされると、ナビゲーションが非常に速く感じられ、upload thingのような私が構築したものよりも速く感じられます。しかし、upload thingは新しいパラダイムを使用しているため、はるかに少ないリソースを使用し、はるかに少ないデータのやり取りで済みます。
私がビルドする際に目指しているいくつかの利点があります。
利点1:ユーザーがリンクをクリックすると、即座にページが変更されるべきです。ユーザーが何かをクリックして、何かが変更されているかどうか分からないという状況は避けたいです。
また、クライアント上でポップアップビデオプレーヤーのようなものを持ち、それが別の場所に移動しても消えないようにすることもできます。
さらに、最初のページロード時にサーバーを待ちたくありません。サーバーがページを生成してからユーザーが見られるようになるまでに、ナビゲーションの度に固有のペナルティがあります。
特にサーバーレスを使用している場合、コールドスタートにヒットすると、サーバーからレスポンスを受け取り始めるまでに数秒かかる可能性があります。スピンアップし、データベースに接続し、静的なアセットを送信していることに気づくまでの間、物事の生成を開始しなければならないからです。これは良くありません。
したがって、ユーザーが何かを見るために、サーバーが完全なページを生成するのを待つ必要があるのは避けたいです。
3つ目に本当に欲しいのは、動的な部分をサーバー上で生成したいということです。ページがHTMLをロードし、JavaScriptをロードしてパースし、JSを実行し、サーバーにヒットし、データを取得してからコンポーネントをレンダリングし、さらに5つのデータを取得する必要があることに気づくという、従来のSPAで最初の3分の1で話したような行ったり来たりを避けたいのです。
ユーザーが実際のコンテンツを見るのをブロックする全てのことを避けたいのです。
これら3つのことは上手く組み合わさりません。なぜなら、サーバー上でレスポンスを生成する場合、そのレスポンスはサーバーから来なければならず、ページがCDNから静的に来ているけれども動的な部分がある場合、本質的にその動的な部分のサーバーで全ページがブロックされることになるからです。
そのため、サーバー上で動的な部分を生成する時、ここでの目標を損なっているように感じます。リンクをクリックしたり、リンクにアクセスしたりして即座にコンテンツを見ることができません。なぜなら、サーバーが全てを生成する必要があるからです。たとえその大部分が静的であっても。
先ほど部分的なプリレンダリングについて触れましたが、これが非常にクールだと思う理由がここにあります。静的な部分はほぼ即座に応答し、動的な部分はサーバー上でリクエスト時に動的に生成することができます。両方の良いところを取り入れていますが、デメリットもあります。
以前に詳しく説明したように、CDNから静的な部分を取得できるエッジワーカーが必要です。最初の部分の生成を待つ必要がないように。CDNから初期HTMLを取得して即座に返し、準備ができたら残りをストリーミングできるのは本当にクールな機能です。
しかし、これには非常にスマートなインフラストラクチャが必要で、CDNからプルするための初期解決を行うこのエッジワーカーが必要です。これはフロントエンドの開発者がインフラの全てのことに精通している必要があるか、Vercelに支払いをしてバックエンドをそこに移行する必要があることを意味します。バックエンドチームがそれをどう感じるか、ご存知でしょう。
これらの機能は、Vercelが独占して他の人には使わせないという意味で排他的なわけではありません。Vercelがそれを構築した数少ない場所の一つだという意味で排他的なのです。
これが、MPAソリューション、つまり分割実行サーバーアイランドモデルがとてもクールな理由です。その解決レイヤーを持つ必要がないからです。
静的なHTMLはどこで動的な部分が追加されるのでしょうか?新しいNextJS Vercelモデルでは、静的な部分と動的な部分の違いはそのエッジワーカー上で識別されます。静的な部分はCDNを通過してユーザーに戻り、動的な部分はサーバーに転送されて戻ってきます。
ここでの実質的な唯一の違いは、以前はVercel上で実行されていたエッジワーカーがもはや存在せず、今はあなたのデバイス上で実行されているということです。
以前は、エッジワーカーがあった場所に、この小さな埋め込みJavaScriptがあります。全く同じことを行っています。サーバーから静的なCDN HTMLを取得し、次にこのJavaScriptが動的な部分を識別し、それをサーバーに生成させます。
その作業を行うボックスが後ろで実行される代わりに、今度はクライアントが実行します。
ここでのコストは、両方を同時に開始できないということです。/cartにアクセスすると、他のモデルではユーザーがHTMLの取得を開始し、サーバーが残りの生成を同時に開始します。なぜなら、両方が同じものによってヒットされているからです。
ここでは、次の部分を開始する前に、ユーザーが物事を取得するのを待つ必要があります。
利点は、これを処理するインフラストラクチャが大幅に単純化され、慣れ親しんだ従来の方法でホストできることです。この部分は単なるS3で、この部分は単純なNodeサーバーやLambdaです。
他の方法で行う場合、インフラストラクチャはより複雑である必要があります。
これらの全ての戦略には利点と欠点があり、動的な部分がサーバーでレンダリングされ、静的な部分が即座に表示され、最初のページロードが素晴らしく感じ、ナビゲーションがマシン上でコードが実行されているように感じる場所に業界として移動していくのを見るのは本当にクールです。
これが私が見たい勝利です。これらの部分が全て一緒になって、使用するツールが正しく動作し、動的な部分と静的な部分について考える必要がなくなることを望んでいます。
現在の問題は、Cloudflare以外のほとんどの事前プロビジョニングされたエッジサービス(NetlifyやVercelなど)は、エッジに到達する前にメインサービスを経由してトラフィックをルーティングする必要があり、それによってエッジが遅くなるということです。その通りです。
これらのモデルには多くの落とし穴がありますが、多くの利点もあります。冒頭で述べたように、このビデオはRyan Carniatoによる「JavaScript Frameworks」の記事によって始まりました。このビデオはすぐに続きます。まだ登録していない場合は、ぜひ登録してください。
これらは全て最終的にまとまります。とても楽しみです。記事を書いてくれたRyanに感謝し、このカオスを最後まで見てくれた皆さんにも感謝します。
本当に、そのJavaScriptフレームワーク2025のビデオを見てください。素晴らしい内容になるはずです。とにかく、ピースアウト。