Ajaxから始まった一つの時代の終わり
最近の流れを見ていての感想文なので、ideaとして投稿します。筆者のバックグラウンドとしては、Remixの商業記事を書いたり、App Routerの商業記事を書いたりしている人です。
さて、筆者は2022年の秋から、社内システムではありますがRemixをプロダクション運用しています。また、Next.jsのApp Routerについても、パラダイムとしてはRemixにインスパイアされた部分が多い[1]おかげで、順調にキャッチアップできています。
RemixとApp Routerは、ルーティングとデータフェッチを高度に統合しており、Progressively Enhanced SPA(PESPA)と呼ばれることもあるそうです。PESPAについては、次の記事が話題になりましたね。
このPESPAであるRemixを実運用する中で、フレームワークの手触りが近年触ってきたものと大きく違っている点があったので、本記事ではそこに言及していきます。
対象読者
- Next.jsのApp RouterやRemixの機能面での表面的な特徴は理解している人
- 上記フレームワークが従来の開発手法と何かが違うのはわかるものの、何が違うのか自分の中で整理できていない人
Ajaxから始まった非同期通信の歴史
ざっくり2000年代の前半までは、Webページへ動的に埋め込むデータの調達の責務は、サーバーのみが担っていました。いわゆるCGIの全盛期です(要出典)。
このようなサーバーサイドが頑張る文化は、Ruby on RailsやASP.NET[2]といった形で正統進化していきましたが、基本形としては次のような形でクライアントとサーバーの関係は成立していました。
- データフェッチ: サーバーサイドで通信した結果を使ってHTMLを生成し、ブラウザはHTMLを受け取って表示する
- データ更新: ブラウザからの
<form>
によるリクエストをサーバーで受け取って処理する
この状況が変わったのが、2000年代の半ばのことです。Ajax[3](エイジャックス)と呼ばれる非同期通信のための機能群が生み出されることによって、JavaScriptはフォームのsubmit処理やページ遷移を伴うことなく、任意のプログラマブルなタイミングでサーバーとの通信を行えるようになりました。この分野では、古くはjQueryの $.ajax()
が活躍し、2015年以降は標準APIとして実装されたFetch API(≒ fetch()
関数)が活躍しています。
これ以降の歴史は、次の記事で素晴らしくまとめられているので、本記事での詳しい言及は避けます。
重要な点は次のあたりでしょうか。
- 素朴なSPAはJSファイルのサイズが肥大化しやすく、初期表示までの速度に難があった
- ユーザー環境のネットワーク速度はサービス提供側でコントロールできないため、初期表示のためのデータフェッチの責務をすべてクライアントサイドで担う構成にすると、パフォーマンスの問題が発生しやすい
- Next.jsでは初期表示のためのデータフェッチの責務をサーバーサイドに寄せる試みがなされた
- App RouterとRSCでは、初期表示以外も含む、ほぼすべてのデータフェッチをサーバーサイドで行う試みが始まっている
ユーザーのネットワーク環境に左右されづらいデータフェッチの必要性については、Remixでも重要な哲学として次のページで語られています。
哲学については筆者がCodeZineで書いた記事を読んでもらったほうがわかりやすいかもしれません。
ブラウザとAPIサーバーの関係がAjax時代とは変わった
さて、Next.jsの当初の構成(Pages Router)では、Webサーバー(本記事では便宜上 BFF と呼ばせてください)は、初期表示やクローラー応答のために使われていました。しかし、RemixやNext.jsのApp Routerという、現代のWebアプリケーション開発における最先端に数えられるフレームワークたちは、どちらもデータフェッチをサーバーサイド(BFF)の責務として回帰させる設計になっています。
まだ触っていない人によく勘違いされがちなのが、Next.jsのPages Routerのような「初期表示時にはデータフェッチしてSSRによるHTML生成を行い、初期表示後には通常のSPAとして振る舞う」という挙動の延長線にPESPAがあると思われていそうな点です。初期表示時にHTMLを生成するまでの流れは、従来のPages Routerの挙動と大きく変わりませんが、初期表示後の動きはかなり異なります。
RemixやApp Routerで、ある画面でデータ更新(更新ボタンを押したり)を行い、画面遷移せずに画面内のデータが更新後のものに書き換えられる機能があったとします。このデータ更新の際、APIサーバー等から最新のデータを取り直す必要があるわけですが、このタイミングでもブラウザからAPIサーバーへの通信は行いません。データフェッチは、Remixなら loader()
関数、App RouterならServer ComponentsがBFF内部で実行することになっているので、データ更新時の処理順は次のようになります。
- ブラウザからBFFにデータを送信する
- Remixでいうaction
- App RouterでいうServer Actions
- どちらも
<form>
から扱うことを主に想定している(JavaScriptから実行する方法もある)
- BFFでAPIサーバーから最新データを取り直す
- BFFからブラウザに加工済みの最新データを渡す
- Remixなら
useLoaderData()
からデータが出てきて、データの差異によってUIを更新する - App RouterならFlight Protocolで表現されたReactツリーが直接ロードされてVDOMの一部を置き換える
- Remixなら
RemixとApp Routerで細部に違いはありますが、基本的にはデータフェッチでもデータ更新でも、ブラウザはBFFのみを相手に喋っています。しかも、自身であるページやコンポーネントに専用で用意されたデータフェッチ経路を使っており、BFF側でこまめな一次加工もできるので、ネットワークを通るデータの量は最小限になっています。
ここで重要なのは、(かなりわかりづらいですが) クライアントとサーバーの見かけ上の関係性がAjax以前の時代に戻っている ということです。 <form>
でデータ更新を行うと、サーバー側(BFF側)でデータをまとめ直して画面を更新してくれる、といった切り口で見た場合に、見かけ上はそう見える、という話ですね。実際には内部でゴリゴリにAjaxしているので、Ajax不要論にはなりませんが、Webアプリケーションを開発するにあたって fetch()
関数やそのラッパーとなるライブラリをブラウザ上で動かすつもりで開発者が扱う機会は極めて少ない、ということです。
注目すべき点として、この挙動はオプションではなくデフォルトであるということです。実際に触ってみるとわかるのですが、RemixもApp Routerも、基本的にクライアントサイドで fetch()
を直接使わせる気がかなり少なめです。そもそもApp RouterではすべてのコンポーネントがRSCとして動作するため、よほど明示的に "use client"
を付けた場合にしかブラウザ側でJavaScriptが実行されません。Remixも理論上はブラウザ上で fetch()
を実行することは不可能ではないはずです[4]が、代替としてuseFetcher()
を整備して、任意のタイミングで任意のパスの loader
や action
を呼び出せるようにしている程度には、 fetch()
がなくても問題ないようにしています。
Ajax偏重の時代の終わり
筆者の個人的な感想として、あえて煽るような言い方をするのであれば、「これは、Ajax偏重の時代を終わらせにきているな」と感じました。RemixもApp Routerも、ルーティングとデータフェッチ(とデータ更新)に関わる大半の責務をBFFに寄せており、ブラウザ側でやることといえば、取得したデータを表示することと、BFFにデータの更新を依頼することだけになっています。
SPAに慣れてきたエンジニアには、少し奇妙な世界観に感じられるかもしれません。しかし、できればこの変化を受け入れて、技術選択の手札の一つとして持つべきです。これはクライアントアプリケーションが非同期処理をする回数を劇的に減らすソリューションであり、多くの場合にアプリケーションの複雑さを軽減します。実際に1年近くRemixを触ってきて、もう非同期通信をクライアントからいちいち扱う複雑さには耐えられないと思うようになりました。慣れるにはある程度のアンラーニングが必要になりますが、それに見合った成果として、開発中に意識すべき複雑さの軽減が期待できます。
これまでの20年弱の期間において、ブラウザが主体的かつプログラマブルにデータを取得できる $.ajax()
や fetch()
はWebアプリケーションにとって劇薬とも言える強力な武器でした。しかし、それを濫用することで開発体験やパフォーマンスを損なうケースが出てくることも、またこの20年弱で学んできたことです。
ユーザーのネットワークは思ったより貧弱で、大きなJavaScriptファイルをダウンロードさせることも、高頻度で通信させることも、ユーザー体験を損なうことがわかりました。開発者のほとんどは天才ではないので、幾つもの非同期通信の結果を適切にUIへ反映させるのは脳に過度な負担をかけることがわかりました[5]。
これらの課題への包括的な解決策、その到達点が「できるだけAjaxしない」によって実現されるのであれば、試す価値があるのではないでしょうか。
まとめ
最近の流れを見ていて、「RemixもApp Routerも、アプリケーション開発者がAjaxを濫用することで苦しんでいた時代を終わらせようとしているなー」という感想を持ったので、長々と書いてみました。
ぶっちゃけ筆者はモバイルアプリ開発が出身でPWAが好きなので、適切なビジネス領域ではPWAもSPAも生き残ってほしいなと切に願っておりますが、世間の大半のビジネス領域ではPESPA的な世界観のほうが学習コストも低そうだし上手くいくんだろうな、と思っています。
ちなみに、今回はRemixやNext.jsにフォーカスして話をしましたが、AstroやSvelteKitあたりもフレームワークの形としては似たようなパラダイムを採用しているので、今後はこういうノリで行くんだろうなあと思って眺めています。
最後に、RemixもApp Routerも、見かけ上、Ajaxを意識する場所が激減していますが、フレームワークの内部実装としてはAjaxをむしろ多用していますので、「Ajax終了のお知らせ」みたいな感想は持たないようにお願いします。Ajax濫用の時代は終わるかもですが、Ajaxはこれからも私たちの大切な基礎技術の一つです。
おまけ(チラシの裏)
ところでさあ、筆者はモバイルアプリ出身だから考えちゃうんだけどさあ、「クライアントがAPIサーバーと直接何回も通信するのはパフォーマンスを損なう」っていう課題はさあ、モバイルアプリにも刺さるんですよ。モバイルアプリにはBFFみたいな頼れる味方がいないので、PESPA的なアプローチでの改善はできないんですよ。
モバイルアプリで通信によるパフォーマンス低下を改善するには、GraphQL的なアプローチしか残されていないのか……?
ひょっとしてひょっとすると、いつかモバイルアプリのためのBFF的なサーバー[6]も生まれるのか……? そんなん作るくらいなら、React NativeをRSC対応させたほうが早くないか……?(※ 2023.9.14追記:React NativeのRSC対応はExpo Routerが取り組むようです)
モバイルアプリのUI開発パラダイムはReactの後追いをしている状況なので、PESPAのムーブメントを見たComposeチームやSwift UIチームが数年後にどんなソリューションを出してきてくれるのか、大変楽しみですね。
-
シンタックスは違うけど機能面では近いものが多い ↩︎
-
他にも多くの素晴らしいフレームワークがあったかとは思いますが、有名どころだけでもそこそこの量があるので2つで勘弁してください ↩︎
-
MDNの記事を読んで知ったんですけど、JSONを扱うFetch APIもAjaxって呼んでいいんですね。XMLやXMLHttpRequestを扱った時にしかAjaxと呼べないもんだと思ってた。 ↩︎
-
Remixを触り初めた頃にブラウザでの
fetch()
がうまく動かなかった記憶があるので、もしかしたら不可能寄りかもしれない ↩︎ -
これはGraphQLによってある程度解決されますが、GraphQLサーバーを上手に運用できる組織はある程度上澄みで、庶民は救えないという認識です ↩︎
-
RSCみたいにSwift UIとかComposeの計算済みのツリーを直接返せるサーバーになるんか?マジで? ↩︎
Discussion
Evan BaconがExpoで試しにやってみたと言った時はワクワクが止まりませんでした。可能性としてありそうなのは楽しみです。
あーそれです!どこで見たか思い出せなかったんですけど、そうでした🥓氏でしたね!
どこかで見たなーと思いながら↓を書いてました。
VDOMのサブツリーを作るだけと考えると、React Nativeでも全然実現可能なはずなんですよねえ
@tell_y 先にExpo Router v3とExpo 50でAPIサーバーを作れるようにして、それからRSC対応へ拡張していくみたいですね!
記事見ました、楽しみですね!
SEOフレンドリー、というのがいつの頃からかうるさく言われたり、1秒の遅延で売り上げが大きく変わると某企業が盛んに啓蒙したこともあるかと思います。
CDNやVercelのエッジなどのインフラの進化もWebの世界を大きく変えました。
Next.jsのSuspenseのような、Wired Formatをストリーミングする通信方式や、サーバー・アクションなどのVercelマジックも楽しみではありますね。
Remix も Next.js の App Router も、内部では、Ajax(Fetch)で非同期通信しているハズですよ、Ajax(Fetch)での非同期通信をユーザー(利用開発者)からは隠蔽しているだけで。(HTMLのformは同期通信なので...。)
はい、ご認識のとおりですね。本文中でも言及させていただいております。
最後(おまけの前)に、書いてあったんですね、見落としてました。。。
はい。それから、私も忘れていましたが、中盤に
という形でも言及していました。
つい先週、Webフロントエンドは変化が早すぎるという話をしていたので
こういった10年レベルでのまとめは参考になります
最近Astroというものを知って、これについても言及されていて抵抗感が減り助かった気がします