戻されているそうです。
シングルページアプリケーションは速度のための銀の弾丸ではなく、使い方を間違うとパフォーマンスは悪化します。
シングルページアプリケーションのフレームワークはコンポーネントで階層構造にしてオーガナイズできるようになっています。それによってモジュール性を高めて再利用性が向上したり、自分のレイヤー以外のことを気にしなくても良くなってコードの見通しが良くなる、という、20年ぐらい前からよく言われていたいつものやつです。
例えば、つぎのようにAというページが、Bというコンポーネントを使い、さらにCというコンポーネントを使っていたとします。
A
+- B
+- C
AはBの中身を知る必要はありません。・・・というのが正しいオブジェクト指向の作法とされています。Bコンポーネントは表示のためにサーバーアクセスが必要だったとします。そして、サーバーから取ってきた情報を分析して表示するタイミングで、コンポーネントCを使うことが必要なことがわかりました。BもまたCの中身は知りません。そして、Cもサーバーから情報を取る必要があったとします。
そうなると、ブラウザが最終的に情報を表示するためには5往復しないといけないことになります。広告みたいにがちゃがちゃとレイアウトが変わったり、いろいろ弊害があったりします。
┌───────┐ ┌──────┐
│Browser│ │Server│
└───┬───┘ └──┬───┘
│ HTML Request │
│────────────────────────────>
│ │
│ HTML │
│<────────────────────────────
│ │
│ JavaScript Request │
│────────────────────────────>
│ │
│ JavaScript │
│<────────────────────────────
│ │
│Request JSON for Component A│
│────────────────────────────>
│ │
│ JSON │
│<────────────────────────────
│ │
│Request JSON for Component B│
│────────────────────────────>
│ │
│ JSON │
│<────────────────────────────
│ │
│Request JSON for Component C│
│────────────────────────────>
│ │
│ JSON │
│<────────────────────────────
JavaScriptも(Mithril以外は)サイズが大きくなる傾向がありますし、それぞれのタイミングで取ってきたコンテンツを解釈したりコード実行したタイミングで次の情報が必要となって・・・という感じなのでそうとう時間がかかります。
ステップ1
シングルページアプリケーションのURL設計がきちんとRESTに従っているのであれば解決策がないわけではありません。
RESTであればURLが決まれば表示する内容はだいたい一意に決まるはずです。
オブジェクト指向の設計の原則を無視して、必要な情報をトップのコンポーネントがすべて把握するようになれば、必要な情報をまとめてサーバにリクエストできるので、リクエストのラウンドトリップは減ります。
HTTP/2だったら、複数のリクエストを並列で飛ばしても問題ありませんが、HTTP/1.1では並列処理ができるかどうかは運次第(細かい画像のリクエストやSSEで並列化しているセッションが食われると並列化できない可能性がある)ので、1回でまとめてリクエストしてまとまったJSONを返すAPIが必要かもしれません、サーバーのAPIはもはやRESTではなくなります。階層構造で整理されたリソース構造にHTTPにアクセスするのではなく、ページで必要な階層をまたいだ色々な要素を一括でもらう必要が出てくるからです。
┌───────┐ ┌──────┐
│Browser│ │Server│
└───┬───┘ └──┬───┘
│ HTML Request │
│───────────────────────────────>
│ │
│ HTML │
│<───────────────────────────────
│ │
│ JavaScript Request │
│───────────────────────────────>
│ │
│ JavaScript │
│<───────────────────────────────
│ │
│Request JSON for All Components│
│───────────────────────────────>
│ │
│ JSON │
│<───────────────────────────────
実現したいこと
- JSONのリクエスト回数を1回に減らす
必要なこと
- ページ内で必要な、サーバにリクエストしないといけないデータの一覧はトップのコンポーネントが確実に知っている
- HTTP/1.1では一括でいろんな情報をまとめて返すRESTfulではないAPIをサーバーが提供する
Mithrilは1.0からラウトリゾルバーというものが追加されました。トップレベルの表示コンポーネントのさらに親となる要素です。このonmatch内で取得してキャッシュしておくことで、トップレベルコンポーネントが起動するときには全部の必要な情報がそろっている、という状態にできます。
ステップ2
これが実現できれば初回リクエストは大幅に改善しますが、ページ切り替えのたびに全部のJSONを再送してもらうことになってしまうかもしれません。
画面更新のための、同一セッション内の2回目以降のリクエストでは、全部のJSONではなくて、自分が持っている情報との差分だけが欲しいはずです。すでにロード済みのデータの一覧がわかっていれば、ページ間で必要な情報一覧が分かれば、その差分だけリクエストすれば良さそうです。
実現したいこと
- 差分更新で必要な情報だけを取得したい
必要なこと
- ロード済みの情報一覧を持っていて、新しいページに必要なJSONの一覧との差分が分かる
ステップ3
さて、以上で初回リクエストも二回目以降の更新も高速にできそうです。
あとは、初回ページのリクエスト時に、そのURLで必要なデータをすべてサーバー側でも把握していれば、事前に埋めこんだり、そのページの表示に特化したJSONがすべて入った.jsファイルを動的生成してそれをHTMLからscriptタグで読み込ませるようにすればさらにラウンドトリップが減らせますね。次のアドベントカレンダーで書いたやつです。
最初はHTMLに埋めこんでおけば、と書いたのですが、CSP的には別ソースの方が良かろう、と当時とは少し違った感想を今は持っています。 Cache-Control: no-cache
にしておいて、毎回リクエストがきちんと飛ぶが、ETagで不要な更新は送らないようにできる、ということができれば良さそうです。
実現したいこと
- JavaScript取得時にはデータもそろっている状態にしたい
必要なこと
- そのURLを表示するのに必要なデータをサーバ側でも完璧に把握しておいて、必要な情報セットが入ったJSONを.jsファイルとして動的生成する
ざつなまとめ
いろいろポエミーに考えたんですが、これらを実現するには、サーバもクライアントも、ページごとに必要な情報一覧を把握しておく必要がありそうです。でもって、ブラウザ側の通信ミドルウェアのレイヤーが適宜差分をリクエストして、キャッシュしている情報を適切に更新したり・・・みたいな感じですかね。共同編集系のウェブサービスであれば、Server Sent EventやWebSocketに必要な最新のJSONを載っけちゃう、というのも面白そうです。
シングルページアプリケーションになって、クライアント側にもRouterが登場しました。サーバーのRouterと同一の設定ファイル(サーバーがnode.jsなら同一ソースもできるかもだけどそうじゃない場合は設定ファイル)を共有して、その設定ファイルには必要なリソース情報が書いてあって・・・みたいな世界ですかね。既存のRouterを完全に乗っ取るわけではなくて、URLを受け取って必要なデータ一覧を返すような、URLをキーにしたMemcachedみたいなやつがいれば良さそうです。
そこまでできれば、シングルページアプリケーションを作ってみたけど遅くなってしまった、みたいなこともなく、作ってみたらサーバーとクライアントで見ている世界(データやテーブルの構造)が違いすぎてつらい、みたいなこともなくなりそうな気がします。
とりあえず趣味コードで、Mithrilのクライアントと、Goのサーバー用に設定を共有してうまいことやってくれるやつでも作ってみますかね。