対象読者
- Reactの基本を修めている方
- 通信回線が弱いユーザーにも高速に表示できるサイトを作りたいエンジニア
- WebブラウザとNode.jsという異なるランタイムをそれぞれキャッチアップするのが辛くなってきたエンジニア
前提環境
筆者の検証環境は以下の通りです。
- macOS Ventura 13.4.1
- Node.js 20.4.0
- NPM 9.7.2
- Remix 1.18.1
Webサイトと通信の歴史
これまでの連載で、Remixの基本的な考え方や、頻出する使い方について解説してきました。今回からは、個別の機能にフォーカスして解説していきます。今回は、通信に関する機能の解説として、データ取得とデータ更新について解説します。
さて、Web標準とプログレッシブエンハンスメントを大切にするRemixは、通信においても可能な限り、ブラウザの基礎的な機能に肉付けする形でユーザー体験をよくするための機能を作り込んでいます。そのため、Remixの挙動を理解するには、ブラウザとサーバーの古き良き基礎的な関係を学ぶことが重要です。順番に見ていきましょう。
1998年のWeb
まずは、20世紀末、1998年ごろのWebにおける通信がどんなものだったかを見てみましょう。ちなみに1998年を選んだのは、JavaScriptが非同期通信を行うための機能である XMLHttpRequest
が生まれたのが1998年頃である説があり、ブラウザが最も素朴な通信方式を採用していた最後の時代だからです。Remixのドキュメント内でも、Remixで実装できる最も素朴なデータ送信環境について「ようこそ1998年へ(Welcome to 1998.)」というジョークが記載されています。
さて、この時代のWebについて、システム的な特徴を確認してみましょう。1998年には、Webサーバーが別の言語に処理を委譲して動的にHTMLを生成する仕組みである、CGIが整備されていました。つまり、あるページ表示のリクエストに対して、データベースに保存されているデータを動的に埋め込んだHTMLをレスポンスとして返し、表示することができました(図1)。
その一方で、ブラウザにはまだJavaScriptで動的に通信する仕組みは無かったため、ユーザーによる操作の結果をサーバーに伝えるには、<form>
要素によるsubmit処理を行うしかありませんでした(図2)。
submit処理の後には、新しいデータを表示したいですよね。シングルページアプリケーションの時代であれば、レスポンスデータを使って動的に画面を更新して、となるところですが、1998年のブラウザにそんなことはできません。
では、どうやって最新のデータを表示していたのかと言うと、図2の③にある通り、submitの完了後に自動でページをリロードするのがデフォルトの挙動だったのです。JavaScriptランタイムから見れば、サーバーへのデータ送信が1回発生するごとにシステムが再起動して、あらゆるものがやり直しになる挙動なので、当時のJavaScriptがどれだけ不遇だったのかがわかりますね。
[コラム]preventDefaultが妨げていたもの
シングルページアプリケーションでフォームのsubmit処理をハンドリングしたことがある人であれば、Event.preventDefault()
というAPIに見覚えがあるのではないでしょうか。読んで字のごとく、デフォルトの挙動が発生しないように妨げるためのAPIです。
このAPIがフォームのsubmit後に妨げていた「デフォルトの挙動」こそが、前述した、ページのリロードなのでした。JavaScriptのメモリ内情報を継続させることでリッチな動作を可能にしているシングルページアプリケーションで、ページのリロードが発生するのは致命的なことです。ブラウザの標準の挙動とアプリケーションとしての動作の自然さの合間で、折り合いをつけるために preventDefault()
は実行されていたわけですね。
Ajaxでアプリケーションとして振る舞えるようになったWeb
1998年ごろにInternet Explorerが独自実装する形で、JavaScriptが非同期通信を行うためのAPIである XMLHttpRequest
が発明されました。その後、後追いで標準化される形で他のブラウザにも実装され、非同期通信に関する機能は2005年からAjax(エイジャックス)と呼ばれるようになりました。
その後、UIの状態管理とDOMツリーの更新を得意とする、Reactのようなフレームワークと組み合わせることで、シングルページアプリケーションと呼ばれるWebサイトが構築できるようになりました(図3)。
一度HTMLを表示したら、そのあとはブラウザ側で動的にAPIサーバーと通信を行い、その結果を使って動的に画面を更新し、基本的にHTMLとしてはリロードしないままで情報が描き変わっていきます。リロードが発生しないことで、JavaScriptのメモリ内の情報が継続するため、リッチかつスムーズなUIが作成しやすくなりました。
シングルページアプリケーションの通信の課題
ただ、何年かこの方式が流行った後に、課題も浮き彫りになってきました。その一つとして、APIサーバーの設計次第で、ブラウザからの通信回数が多くなることが挙げられます。
理想的には、1つのユーザーアクション(画面表示やデータ送信)に対して画面を構築するために必要な通信の回数が1回で済むAPIサーバーを設計したいですよね。しかし、メンテナンスコストなどの事情から、データベースのテーブルと1対1で対応したエンドポイントを持つREST APIを設計してしまうことも少なくないでしょう。そういった場合、1つのユーザーアクションに対して、数回から場合によっては10回以上の通信を行わないと、画面を構築するためのデータが用意できないかもしれません。
ここはソフトウェアエンジニアの悪い癖で、まあ10回くらいなら大した時間ではないだろう、と考えてしまいがちなのですが、この判断には悪いバイアスがかかっています。ソフトウェアエンジニアの労働環境に整備されているインターネットは、世間一般から見ると高速な部類に入りがちなのを忘れているのです。
ユーザーは、そのWebサイトを、さほど高速ではない通信キャリアのモバイル通信回線で、スマートフォンから見ているかもしれません。パソコンから見ているのだとしても、固定回線ではなくモバイルルーターを据え置き型にした、事実上のモバイル通信回線でアクセスしているのかもしれません。何にせよ、ソフトウェアエンジニアのデバッグ中の体感速度よりも、ユーザーは遥かに遅い通信回線で、画面のローディングUIを長時間眺めているケースは、少なからず存在していると思っておいたほうがよいでしょう。
大きいサイズのデータを取得することも、高頻度の通信を行うことも、どちらもユーザー体験を損ないます。そして、ユーザーのネットワーク環境は、自社で契約しているサーバーと違い、多少のお金をかけた程度では改善することは困難です。通信の回数を減らすこと、通信のサイズを減らすことは、Webサービスの提供者にとっても大きな関心ごとになってきているのです。
また、副次的な話題ではありますが、開発体験の観点でも問題があります。一般論として、非同期処理は扱いが少し難しいものです。Promiseやasync/await構文などによって、扱いやすくなってきたものの、やはり10回以上の非同期通信の結果を、適切に待ち合わせてUIへ反映する処理を組み立てる作業は、開発者の脳を酷使します。
ユーザー体験や開発体験を向上するためには、ブラウザが行う通信の回数やサイズを減らせる仕組みが必要になってきているのです。