この記事はAteam LifeDesign Advent Calendar 2023 の11日目の記事です。
はじめに
最近Next.jsのプロジェクトからRemixのプロジェクトの開発を行うようになりました。
実際に開発してみて、なんとなく違いはわかるんだけど上手く説明はできなかったり、そもそもRemixがどういう背景で開発されたのか知らなかったため調べてみました。
Remix vs Next.js というブログがあったので内容をかいつまんでまとめました。
進化するウェブ標準への適合、パフォーマンスの最適化
なぜNext.jsは高速なのか?
Next.jsは、静的サイト作成(SSG)を使っています。Next.jsはビルド時にサーバーからデータを取得し、ページをHTMLファイルにレンダリングして公開ディレクトリに起きます。サイトがデプロイされると、静的ファイルは1カ所のオリジンサーバーに送信されるのではなく、エッジで送信されます。リクエストが来ると、CDNは単にファイルを提供します。データのロードとレンダリングは前もって行われているので、サイトの訪問者はダウンロードとレンダリングの時間を待つ必要がありません。また、CDN(エッジ)はユーザーの近くにグローバルに分散されているため、静的に生成されたドキュメントのリクエストは、単一のオリジンサーバーまで到達する必要がありません。
RemixはSSRしかないのに、なぜ早いのか?
RemixはSSGをサポートしていませんが、HTTP stale-while-revalidate
キャッシュディレクティブ(SWRやVercelのswrクライアントフェッチパッケージとは別物)が使われています。最終的にはエッジに到達するのでNext.jsと結果は同じですが、違いは、リクエストがそこに到達するまでの方法です。
ビルド/デプロイ時に全てのデータをフェッチしてページを静的にドキュメントにレンダリングする代わりに、キャッシュは実際にユーザーがページを閲覧したときに呼び出され、必要に応じて保存されます。ページはキャッシュから提供され、次のサイトの訪問者のためにバックグラウンドで再検証されます。SSGのように、リクエストがあるときにダウンロードとレンダリングを待つ必要がありません。
Remixのキャッシュの書き換えが速い理由
RemixではSSGやSWRを使ってCDNでページをキャッシュする代わりに、「エッジ」と呼ばれるネットワークの分散ポイントにデータをキャッシュする手法を採用しています。
さらにRemixアプリケーション自体もエッジに配置します。例えばFly.ioですが、世界中に分散したサーバーを提供するクラウドプラットフォームでアプリケーションをユーザーにより近い物理的な場所で実行することで、パフォーマンスを向上させることができます。
Remixには高速な画像最適化機能も備えられ、処理された画像ファイルは「永続ボリューム」と呼ばれる長期保存用のストレージに保存されます。これにより、画像を効率よく配信することができます。
これらの機能の組み合わせにより、Remixは自らCDN(コンテンツ配信ネットワーク)のように機能することができます。CDNは、コンテンツを世界中の複数のサーバーにキャッシュして、ユーザーの近くのサーバーから高速にコンテンツを配信するシステムです。
要するに、Remixは改良を重ねることで、高速なレスポンスと効率的なコンテンツ配信を可能にする最新の技術を積極的に活用していると言えます。
問題解決のアプローチ
1.動的ページの読み込み
RemixとNext.jsには多くの違いがありますが、アーキテクチャ上の大きな違いのひとつは、RemixはSSGに速度を依存していないということです。
Webアプリケーションの開発において、最終的にSSGがサポートできない壁にぶつかります。たとえば検索ページがそれにあたります。
その問題は、ユーザーが無限のクエリを送信できることです。検索ページなどのクエリパラメータが自由に設定できるページにSSGは設定できません。
SSGは動的なページにはスケールしないので、Next.jsはユーザーのブラウザからCSRのデータ取得に切り替えました。これによりNext.jsのアプリケーションはRemixよりもパフォーマンスが2,3倍遅くなってしまいます。
ウェブパフォーマンスで最も重要なことは、ネットワークウォーターフォールの並列化と考えています。Remixでは、このことに注力しています。
CSRが遅い理由
Next.jsは、私たちが「ネットワーク・ウォーターフォール・リクエストチェーン」と呼んでいるものを導入しました。ここではSSGが使えないので、アプリはユーザーのブラウザから検索結果をフェッチしています。データをフェッチするまで画像をロードできず、JavaScriptをロード、解析、評価するまでデータをフェッチできません。
クライアントでフェッチするということは、ネットワーク上でより多くのJavaScriptを扱うということでもあり、JavaScriptの解析に多くの時間がかかるということでもあります。
RemixがSSRしかないにも関わらず、静的ページと同じくらい速い理由
SSGは検索ページをキャッシュすることができませんが、RemixバージョンはSWRかRedisを使ってキャッシュすることができます。ページを生成する単一の動的な方法があれば、アプリケーションのコードを変更することなくキャッシュ戦略を調整することができます。その結果、よく訪問されるページのSSG速度が向上します。
2.動的ページのキャッシュミス
Next.jsの場合
Next.jsはデータをロードするまで画像をロードできず、JavaScriptをロードするまでデータをロードできず、ドキュメントをロードするまでJavaScriptをロードできません。ユーザーの待ち時間は、そのすべての「待ち」のステップ数の乗数に比例します。
Remixの場合
Remixでは、唯一待たなければならないのはドキュメントが画像をロードできるようになることです。Remixでは、画像をロードできるようにページロードを待てばあとは全て並行してデータフェッチができます。常にサーバーでフェッチするというRemixのデザインは、指数関数的に待ち時間が増えるということはありません。
3.アーキテクチャの乖離
Next.jsがCSRに移行したときに打撃を受けたのは、ロード時間が長くなりユーザー体験が悪くなることだけではありませんでした。このアプリは、サーバーと通信するために、SSG用とCSR用の2つの異なる処理を用意する必要がありました。同じ機能に対して異なるコードが必要になるため、開発の複雑さが増してしまいます。
この処理の分離は、以下の問題を引き起こします。
- ブラウザで認証する必要があるのか?
- APIはCORSをサポートしているか?
- API SDKはブラウザで動作するのか?
- ビルドとブラウザのコード間でどのようにコードを共有するのか?
- APIトークンをブラウザで公開しても問題ないか?
- すべての訪問者に送信したトークンにはどのようなパーミッションがあルカ?
- この関数はprocess.envを使用できるか?
- この関数はwindow.location.originを読みこめるか?
- 両方の場所で動作するネットワークリクエストを作成するにはどうすれば良いか?
- これらのレスポンスをどこかにキャッシュできるか?
- 両方の場所で動作する同型のキャッシュオブジェクトを作り、異なるデータ取得関数に渡すべきか?
Remixは、処理をサーバー側に用意するだけでよいので上記の問題は起こり得ません。
4.クライアント側の遷移
どちらのフレームワークもリンクプリフェッチによる即時遷移を可能にしていますが、Next.jsはSSGから作成されたページに対してのみこれを行います。検索ページ等ど動的なページに適用することはできません。
しかし、Remixはデータ読み込みにアーキテクチャの分岐がなかったので、どんなページでもプリフェッチできます。未知の、ユーザー主導の検索ページをプリフェッチすることは、既知の静的なページをプリフェッチすることと変わりません。
import { Form, PrefetchPageLinks } from "@remix-run/react";
function Search() {
let [query, setQuery] = useState("");
return (
<Form>
<input type="text" name="q" onChange={(e) => setQuery(e.target.value)} />
{query && <PrefetchPageLinks page={`/search?q=${query}`} />}
</Form>
);
}
RemixはHTMLのを使っているので、実際にリクエストを行うのはRemixではなくブラウザです。Remixはこの非同期処理のためにコードを書く必要はありません。
開発者体験の向上
1.シンプルであること
Next.jsとの大きな違いは、Next.jsにはページ上のデータを取得するための4つの「モード」があることです。
- getInitialProps - サーバーおよびクライアントサイドと呼ばれます。
- getServerSideProps - サーバーサイドで呼び出されます。
- getStaticProps - ビルド時に呼び出されます。
- client fetching - ブラウザから呼び出されます。
Remixにはloaderしかありません。そのためよりコードがシンプルに保たれます。
2.エラーハンドリング
もしNext.jsとRemixで作られた通販サイトがあったとします。「カートに追加」ボタンをクリックした時に、バックエンドハンドラーがエラーを投げるとどうなるでしょうか?
Next.jsの場合
何も起こりません。エラー処理は難しく、煩わしいです。多くの開発者は、面倒なのでエラー処理をスキップしてしまうかもしれません。
Remixの場合
Remixはアプリのデータやレンダリングに関するあらゆるエラーを処理します。
アプリのルートでエラー境界を設定するだけです。
Remixがあなたのためにしてくれることは、小さなバンドルとシンプルな変異APIだけではありません。
Remixはサーバとのやり取り(データロードとデータ変異の両方)をすべて処理するので、Webフレームワークの分野では、Webアプリの長年の問題を解決するユニークな能力を持っています。
3.リクエストの中断
ユーザーは誤ってボタンを2回クリックすることがよくあり、ほとんどのアプリはそれにうまく対処できません。しかし、時にはユーザーが複数回クリックすることを想定し、UIが即座に反応することが必要な要件になることがあります。
通販サイトにおいて、ユーザーはカート内の商品の数量を変更することができ、数量を増やすために素早くクリックするケースを想定しましょう。
Next.jsの場合
このコードは、競合状態、中断、再検証を管理していないので、UIはサーバーと同期していない可能性があります。リクエストの中断を管理し、変異後のデータを再検証すれば、このようなことは防げたでしょう。
レースコンディションや割り込みに対処するのは難しいです。Vercelチームは業界で最も才能のある開発チームの1つですが、彼らでさえそれを省略しています。
実際、前回のブログ記事でReact Coreチームが作ったReact Server Componentsのサンプルを移植したときにも、これと同じバグがあ離ました。
Remixの場合
Remixは中断されたリクエストをキャンセルし、POSTが完了した後にデータを再検証していることがわかります。これにより、(このフォームだけでなく)ページ全体のUIが、フォームがサーバーと行った変更と同期していることが保証されます。
もしかすると、Next.jsアプリよりも私たちのアプリのほうが細部にまで気を配っているのではないかと思われるかもしれません。この動作はアプリケーションのコードにはありません。すべてRemixのデータ変異APIに組み込まれています。(ブラウザがHTMLフォームでやっていることをそのままやっているだけです。)
Remixのクライアントとサーバー間のシームレスな統合と移行は、これまでにないものです。
感想
- ここ数年でCDNでできることが増え、もっとアプリケーション側の処理をシンプルにできると考えた
- サイトのSEO対策や、パーソナライゼーションのことを考えるとサイト開発を進めていくと結局SSG、CSRが使えなくなってくる
- であればSSR+CDNでもっとシンプルに実装できるWebアプリケーションのフレームワークがあると良い
というモチベーションの中開発されたのではないかと考えました。
Remix使わなくても、Next.jsで全部SSR+CDNを使えばよいのでは?と思いましたが、
getServerSidePropsを使えばRemixと同じことができるという人もいます。
しかし、そうするためにはgetServerSideProps、API、そしてstate管理(エラー処理、中断、競合状態、リダイレクト、再バリデーションを含む)のためにそれらと通信する独自のコードを実装しなければなりません。
もはやそれは自作のRemixであり、わざわざNext.jsを選択する必要はないでしょう。
もしNext.jsがCSRから離れ、SSRを使うようになれば、おそらくRemixとのギャップは縮まり、より近しいフレームワークとなっていくことでしょう。Next.jsのドキュメントが、サーバーフェッチからSSGやクライアントフェッチに頻繁に移行するように促しているのは興味深いことです。
ということらしいです。今後Next.jsとRemixお互いが影響しあってどういったフレームワークに成長していくのか楽しみです。