最近の流れを見ていての感想文なので、ideaとして投稿します。筆者のバックグラウンドとしては、Remixの商業記事を書いたり、App Routerの商業記事を書いたりしている人です。
さて、筆者は2022年の秋から、社内システムではありますがRemixをプロダクション運用しています。また、Next.jsのApp Routerについても、パラダイムとしてはRemixにインスパイアされた部分が多い[1]おかげで、順調にキャッチアップできています。
RemixとApp Routerは、ルーティングとデータフェッチを高度に統合しており、Progressively Enhanced SPA(PESPA)と呼ばれることもあるそうです。PESPAについては、次の記事が話題になりましたね。
https://www.epicweb.dev/the-webs-next-transition
このPESPAであるRemixを実運用する中で、フレームワークの手触りが近年触ってきたものと大きく違っている点があったので、本記事ではそこに言及していきます。
ざっくり2000年代の前半までは、Webページへ動的に埋め込むデータの調達の責務は、サーバーのみが担っていました。いわゆるCGIの全盛期です(要出典)。
このようなサーバーサイドが頑張る文化は、Ruby on RailsやASP.NET[2]といった形で正統進化していきましたが、基本形としては次のような形でクライアントとサーバーの関係は成立していました。
<form> によるリクエストをサーバーで受け取って処理するこの状況が変わったのが、2000年代の半ばのことです。Ajax[3](エイジャックス)と呼ばれる非同期通信のための機能群が生み出されることによって、JavaScriptはフォームのsubmit処理やページ遷移を伴うことなく、任意のプログラマブルなタイミングでサーバーとの通信を行えるようになりました。この分野では、古くはjQueryの$.ajax() が活躍し、2015年以降は標準APIとして実装されたFetch API(≒fetch() 関数)が活躍しています。
これ以降の歴史は、次の記事で素晴らしくまとめられているので、本記事での詳しい言及は避けます。
https://zenn.dev/suzu_4/articles/2e6dbb25c12ee5
重要な点は次のあたりでしょうか。
ユーザーのネットワーク環境に左右されづらいデータフェッチの必要性については、Remixでも重要な哲学として次のページで語られています。
https://remix.run/docs/en/main/pages/philosophy
哲学については筆者がCodeZineで書いた記事を読んでもらったほうがわかりやすいかもしれません。
さて、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内部で実行することになっているので、データ更新時の処理順は次のようになります。
<form> から扱うことを主に想定している(JavaScriptから実行する方法もある)useLoaderData() からデータが出てきて、データの差異によってUIを更新する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偏重の時代を終わらせにきているな」と感じました。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の計算済みのツリーを直接返せるサーバーになるんか?マジで?↩︎
株式会社モニクルは、「金融の力で、安心を届ける。」をミッションとする金融サービステック企業です。
もしこの記事を読んで会社にも興味を持っていただけたら、↓ の Culture Deck(会社説明資料)を読んでみてください。
https://speakerdeck.com/moniclegroup/culture-deck
エンジニア採用サイトはこちら
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。
Evan BaconがExpoで試しにやってみたと言った時はワクワクが止まりませんでした。可能性としてありそうなのは楽しみです。
SEOフレンドリー、というのがいつの頃からかうるさく言われたり、1秒の遅延で売り上げが大きく変わると某企業が盛んに啓蒙したこともあるかと思います。
CDNやVercelのエッジなどのインフラの進化もWebの世界を大きく変えました。
Next.jsのSuspenseのような、Wired Formatをストリーミングする通信方式や、サーバー・アクションなどのVercelマジックも楽しみではありますね。