Adventar における Server Side Rendering の導入

Adventarを支える技術 Advent Calendar 2019 の9日目です。

Adventar のフロントエンドは Nuxt.js で SPA な構成ですが、一部で Server Side Rendering(以下 SSR)をおこなっています。今日は SSR をする理由や Adventar での導入方法について書きます。

なぜ SSR するのか

SPA で SSR が必要な理由は主に

  • マシンリーダビリティ(SEO とか OGP とか)
  • パフォーマンス

の2つだと思っています。SPA は基本的に JavaScriptAPI を呼び出してコンテンツをレンダリングするので、JavaScript を理解できないクライアントには正しいコンテンツを提供できません。典型的な例は検索エンジンクローラーや、OGP などでサイトのコンテンツを取得するケースです。

SEO については最近のクローラーJavaScript を理解してくれるので SSR 必須ではないと思います。OGP は、Adventar においてはカレンダーページのタイトルと概要テキストだけどうにかできればいいので、Edge Side(Lambda@Edge など)でどうにかして差し込むなどの方法があるかもしれません。

一方パフォーマンスについてはちゃんとやればかなり効果があると思っています。一般的な SPA をレンダリングする場合ユーザーがアクセスしてコンテンツをレンダリングするまでには

  1. HTML を取得する
  2. JavaScript を取得する
  3. JavaScript を実行して API コールする
  4. API の結果を元に DOM を構築する

というフローがあります。SSR だと最初の HTML を受け取った時点でコンテンツ情報もできているので、ユーザーは即座にコンテンツを閲覧することができ、パフォーマンス的には優位です。

もちろん SPA が返す、JS と CSS 以外何もコンテンツがない HTML と比べると、SSR の HTML をつくる過程にはサーバー側で API コールなり、SQL の呼び出しがあり、それを元に HTML を構築する処理が走るので、SPA のような を作るよりも時間がかかります。とはいえ、ネットワーク的なレイテンシの少ない SSR のほうが基本的に速くなるほうが多いでしょう。しかし、サイトの特性によっては初回ロードの遅延がそこまで気にならない場合もあるでしょう。

一方で、SSR によるパフォーマンス的な最大のメリットは、キャッシュによるパフォーマンス最適化がやりやすいところにあると思っています。SSR した結果を CDN などでキャッシュして適切にパージができれば、パフォーマンスは超速になります。

ただし、このパージがけっこう大変で、当初は CDN でキャッシュするアーキテクチャを前提で設計していましたが、色々と考えた結果パージに思ったよりコスト(実装コスト、お金的なコスト両方で)がかかるので結局 CDN のキャッシュは今年はいったん諦めて、リクエスト毎に SSR しています。諦めた理由などについては後日詳しく書こうと思います。来年やれたらやりたいです。

このように、ちゃんとやればいくつかのメリットがある SSR ですが、正直実装コスト、運用コストが高すぎてメリットに見合うかは微妙なところだと思っています。実際、今回実装してみてそれを実感しました。

やはりブラウザとNode.jsという実行環境が全然違う環境で同一のコードを実行するというのは、理論上は可能でも色々と困難です。5日目の記事にもその一部を書きましたし、この他にも実装コスト、運用コストがかなり高いです。

今回 Adventar で SSR を導入するのに費用対効果はあまり考えてなくて、単なる技術的なチャレンジという意味合いが強いです。Lambda を使ったサーバーレスな環境での SSR というのを試したかったので導入しました。

どこに SSR を導入するか

今回、まず決めたのは SSR において認証情報を扱わない、ということです。今回は認証に Firebase Authentication を利用しているのもあり、認証をクライアントサイドで行っており、最初のリクエストにセッション情報をリクエストしていません。

Cookie に認証情報を乗せて頑張ればできないことはないのですが、API サーバーと SSR のサーバーで認証の機構を共有するなど、かなりの実装コストがかかりまし、SSR した結果を CDN でキャッシュする場合、レスポンスの HTML に認証された状態のものを乗せるのは複雑度が爆増するのが目に見てみます。

また、SSR した結果をキャッシュに乗せることを考えると、キャッシュのパージ戦略を考える必要があります。キャッシュのパージは対象のコンテンツとページが多くなるほど複雑になって制御が難しくなるので、できるだけ局所的にするのが良いと思っています。そのような理由から、今回 SSR するのはカレンダー詳細ページだけに限定することにしました。カレンダー詳細ページというは以下のような画面です。

https://adventar.org/calendars/3860

この画面は、ソーシャルメディアなどでもよく共有されるので、OGP を SSR で埋め込む必要もあるし、アクセスの8割はこの画面に最初に着弾するので、初期描画のパフォーマンスが最適化されることに大きい意味があるためです。

SSR 画面を限定したことで、すべて SSR するよりはトータルの実装コストは低くなったとは思いますが、最初にロードする画面が SSR 対象(カレンダー詳細画面)か、そうでないかによって微妙に挙動が変わって、どちらかでしか起きないバグが発生したり、デプロイのフローが複雑になったりと、それはそれで大変でした。

まとめ

今回は SSR のメリットや、Adventar における導入事例を紹介しました。明日はもう少し具体的な、Nuxt.js を Lambda で SSR する場合の技術的なトピックについて紹介したいと思います。