Next.js と Server-side Rendering をプロダクト環境で3年運用してきた知見と率直な所感
こんにちは、医療プラットフォーム本部・プロダクト開発室・第1開発グループ所属の加藤です。 オンライン診療・オンライン服薬指導アプリ「CLINICS」の開発を担当しています。
今回は CLINICS で採用している Next.js と Server-side Rendering (SSR) についてお話ししたいと思います。
Next.js は昨今注目を集めている React ベースの Web フレームワークです。 これから Web フロントエンドの開発を始めるにあたって採用を検討している方も多いのではないでしょうか。
Next.js といえば React コンポーネントをサーバー上で実行して HTML を返す SSR に対応しているのが大きな特徴です。 SSR には様々なメリットがある一方で、採用にあたって自分たちのプロダクトに SSR は不要なのではないかといった懸念や Node.js の実行環境の運用コストを気にする声もよく耳にします。
この記事ではメドレーでの Next.js, SSR の採用事例を紹介しつつ、採用にあたって一般に懸念される点を実際に採用してみた上での所感も交えてお話しできればと思っています。
メドレーの医療プラットフォーム事業と CLINICS アプリについて
まずはメドレーの医療プラットフォーム事業と CLINICS アプリについて簡単に紹介させてください。
メドレーの医療プラットフォーム事業は、2016年にオンライン診療システムとしてリリースされた「CLINICS」を原点としています。 当時は医療機関の方が操作する業務システム部分と患者さん向けの iOS/Android アプリとその Web 版が存在するという構成でした。
その後 CLINICS には電子カルテなどの様々な機能が追加され、現在ではオンライン診療に留まらず医療機関の様々な業務を支援するクラウド診療支援システムへと発展しています。 また、2020年にリリースされたかかりつけ薬局支援システム「Pharms」や2022年にリリースされたクラウド歯科支援システム「Dentis」もプロダクトのラインナップに加わり、より多くの医療機関の業務を支援するプラットフォームとしてサービスの規模を拡大しています。
医療機関向けの業務システムは医科・歯科・調剤薬局とそれぞれの業務特性に合わせてプロダクトが分かれている一方で、患者さん向けの機能はすべて CLINICS アプリに集約されています。 業務システムの機能拡充に伴って CLINICS アプリの機能も拡充されており、現在ではオンライン服薬指導機能や処方箋の事前送信機能など様々な機能が追加されています。
今回お話しするのはこの CLINICS アプリの Web 版 (以下、CLINICS) のフロントエンド開発についてです。
Next.js 採用のモチベーション
CLINICS アプリはリリース以後続々と機能が拡張されてきたのですが、こうした開発を支えるため 2021年に大規模なリニューアルを実施しました。 特に Web 版はリニューアルのタイミングでフルスクラッチで実装し直す選択をしています。
リニューアル以前の CLINICS はログインの前後で実装が分かれており、この点がフルスクラッチでの実装を選択する大きなモチベーションとなりました。 CLINICS は開発当初から Ruby on Rails で開発されているのですが、患者さん向けのページは Rails のテンプレートエンジンを用いて実装されている部分と SPA として実装されている部分が混在していました。 医療機関の検索や詳細などの検索エンジンからの流入を確保したいページではテンプレートエンジンを用いて HTML を直接レスポンスする、ログインページや予約ページなどの操作性を重視したページでは SPA として実装されている、といった具合です。
こうしたテンプレートエンジンと SPA の併用構成では、ログインの前後で一貫したユーザー体験を提供するのは困難だと感じていました。 例えば、同じ機能がログイン前と後とで2つ存在する箇所もあり、ユーザーから見るとログインを挟んで UI が変わってしまい、あまり良いユーザー体験とは言えない状態でした。 もちろんこういった課題はログイン前後の両方のページの UI を丁寧に設計することで解決が可能ではありますが、異なる技術スタックで同じような機能を実装しつつ UI の一貫性を保つのは容易ではありません。 そういった背景からリニューアルではログイン前後での実装を一本化する方針を立てていました。
実装を一本化する上で SSR に対応している Next.js は非常に魅力的な選択肢でした。 SSR には初期表示のパフォーマンス向上などのメリットもあるのですが、採用にあたっては SEO 面でのメリットを重視していました。 近年では Google のクローラーが JavaScript を実行してくれるのですが、直接 HTML をレスポンスできるのであればその方がより確実だと判断しました。 これが Next.js を採用するに至った最大の理由です。
ちなみに、この当時は App Router がリリースされる前だったため、Pages Router で開発を開始しました。 以降の内容も基本的に Pages Router を前提としてお話ししていきますが、App Router や他のフレームワークを使用している場合でも SSR についての考え方は共通している部分が多いので少しでも参考にしていただけたら幸いです。
Server-side Rendering の仕組み
Server-side Rendering (SSR) とは、Next.js や Remix などの React ベースのフレームワークが持つ機能のひとつです。
SSR という用語は昨今の React に関する話題の中ではよく耳にする言葉ですが、実は React の公式ドキュメントに SSR とは何かという記述は存在していません。
それでは React 本体にはサーバー環境向けの機能が存在しないのかというとそうではなく、React はコンポーネントから HTML を生成するための API を提供しています。
Next.js や Remix はこうした API を利用してサーバー環境で HTML を生成しつつブラウザ環境では従来の SPA のような振る舞いを実現する機能を提供しており、こうした機能には SSR という名前が付けられています。
例えば、Next.js の Pages Router では getServerSideProps
を使用してリクエスト毎に HTML を生成する機能を SSR と呼んでいます。
各フレームワークによってサーバー側でのデータ取得の方法は異なりますが、React コンポーネントから HTML を生成する仕組みは共通しています。
サーバー上で HTML を生成すると言うとテンプレートエンジンなどを用いる場合と変わらないように感じられるかもしれませんが、SSR にはハイドレーションという仕組みが存在するのが大きな特徴です。 生成された HTML はクライアントにレスポンスされてブラウザ上で表示されるのですが、この時点ではイベントハンドラーが DOM にアタッチされておらず、ユーザーの操作に対して反応することができない状態です。 そこで React はブラウザ上で再度コンポーネントをレンダリングしてイベントハンドラーを DOM にアタッチします。 この処理はハイドレーションと呼ばれています。
ハイドレーションが行われるとアプリケーションはユーザー操作に反応することができるようになります。 アプリケーションによってはいわゆる Client-side Rendering (CSR) と呼ばれる useEffect によるデータの取得と追加の描画が行われる場合もあります。 さらに、ハイドレーションが行われた状態からページ遷移が行われる場合には history API が使用されます。
一連の流れをシーケンス図にすると以下のようになります。
このように SSR を使用しているアプリケーションでもハイドレーションが行われた後は従来の SPA と同様に振る舞います。 そのため、Context API や Redux などの状態管理ライブラリを使用してページを跨いで状態を共有することも可能です。 こういった特徴から考えると、SSR はこれまでの React と対立するものではなく、むしろその機能を一層強化するものと言えるでしょう。
CLINICS での活用事例の紹介
ここからは実際にどういった形で Next.js と SSR を利用しているのか、CLINICS の活用事例を紹介していきます。
CLINICS のシステム構成は以下のようになっています。 (実際にはもう少し複雑な構成を取っているのですが、今回の説明では簡略化しています)
CLINICS の Next.js サーバーは固有のデータベースを持っておらず、必要なデータはすべてバックエンドの API から取得しています。 SSR の際には Next.js サーバーから、CSR の際にはブラウザから API にリクエストが送信されます。
セッション情報については API サーバー側で管理しており、Next.js サーバー側にはセッション情報を共有しない方針を取っています。 SEO 面でのメリットを重視していたため、SSR するのはログイン不要で閲覧可能なページのみで十分だろうと判断しました。 そのため、SSR をする際にはログイン不要で取得できる情報のみを表示し、ログインが必要な情報を表示する際には CSR を使用しています。 SSR を使用しているページでも部分的に CSR を組み合わせて使用しており、ページによっては SSR を用いずに全体が CSR で実装されているケースもあります。 感覚的には従来の SPA をベースとして部分的に SSR を組み合わせていると言っても良いかもしれません。
採用して良かったと感じている点
ここからは実際に Next.js を使ってみた所感を交えてお話ししていこうと思います。
まずは、Next.js を採用して良かったと感じている点についてです。
全体的な開発体験
フルスクラッチで実装し直す上で Next.js は良い選択だったと感じています。 Next.js というと SSR のイメージが強いかもしれませんが、SSR を抜きにしても SPA の開発に必要な要素は一通り備わっています。 Webpack やトランスパイラなどのビルドツールの設定がデフォルトで含まれており、React のプロジェクトを新規に立ち上げる際には非常に便利なフレームワークだと感じています。 CLINICS では CSS 関連で追加のライブラリを使用しているのを除けば、ほとんどの実装を Next.js と React の標準機能だけで開発しています。
SEO 面での扱いやすさ
こちらは Next.js を採用するにあたって重視していた点なのですが、やはり SSR でサーバーから直接レスポンスを返せるという点で SEO 面では扱いやすいと感じています。 title タグや meta タグなどももちろんですが、HTTP ステータスコードも制御できるため、クローラーに意図を伝える伝統的な方法がそのまま使える点は非常に便利です。 SEO とは直接関係ありませんが、SNS でリンクがシェアされる際の OGP 用の meta タグを動的に生成したいといったケースへの対応も容易です。
とはいえ、近年は Google のクローラーも JavaScript を実行してくれるので SPA が必ずしも SEO 的に不利というわけではありません。 クローラーが JavaScript を理解する能力は年々向上しているので、適切に設計された SPA であれば SEO 的に問題ないというケースも多いようです。 こうしたクローラーの進歩や実装上のテクニックも踏まえると SSR の SEO 面でのメリットはあくまでその扱いやすさという観点で考えると良いかもしれません。
パフォーマンスの向上
SSR を取り入れるにあたってパフォーマンス面について特に意識していたわけではないのですが、実際に SSR を使ってみて FCP (First Contentful Paint) や LCP (Largest Contentful Paint) といった初期表示の速度を表す指標の改善に効果があるなと感じています。 CSR では JS でのデータ取得が完了して始めて LCP 要素が表示されるケースが多いため、JS ファイルのダウンロードや実行の分だけ FCP や LCP が遅延しやすい傾向にあります。 SSR ではこうした JS のダウンロードや実行の時間が減る分、初期表示のパフォーマンス改善が期待できます。 体感的にもローディングインジケーターの表示を挟まずにコンテンツが表示されるため、ユーザーにとってはより快適な体験となるのではないかと感じています。
また、SSR 以外にも Next.js は画像の最適化やコードスプリッティングなどのパフォーマンス向上に役立つ機能を多く提供しているため、安定したパフォーマンスの Web アプリケーションを開発する上では非常に心強い存在です。
セッション管理についての反省点
Next.js そのものについて不便に感じている点はあまりないのですが、SSR を取り入れるにあたってセッション管理についてはよく考えておくべきだったと反省している部分もあります。
従来の SPA であればセッション管理は API サーバーが管理するというのが一般的ですが、SSR を採用する場合では Next.js サーバーと API サーバーが存在するという構成になるケースが多いかと思います。 この場合、Next.js サーバーと API サーバーのどちらでセッションを管理するのか、セッション情報を共有するのか、共有するのであればどのような方法で共有するのかといった点で複数の選択肢が考えられます。 CLINICS では前述の通り API サーバーでセッションを管理して Next.js サーバーとの間でセッション情報を共有しない構成を採用しています。
こうした構成の都合で Next.js サーバーでセッション情報を扱うことができないため、ログイン済みのユーザー固有の情報は SSR することができず、CSR で取得して描画する必要があります。 このようにして SSR と CSR を組み合わせた場合、CSR で取得したデータを描画する際にレイアウトシフトが発生しやすくなります。 また、ログイン済みのユーザーに対しては検索結果をカスタマイズしたり並び替えたりといった要件への対応も難しくなってしまいます。 こういった面で SSR と CSR を組み合わせざるを得ない現状の認証構成には課題があると感じています。
こうした課題を解消するためにも、今後はセッション情報を Next.js サーバーと共有する構成へと変更し、CSR している部分を SSR する形に変更していきたいと考えています。
このように SSR を活用する場合には従来の SPA よりもセッション管理の選択肢が増えるため、セッション管理については特に慎重に考える必要があると感じています。 SSR を今は必要としていないというケースでも、将来的な導入に備えてセッション管理について少し考えを巡らせておくと良いかもしれません。
Next.js の採用を検討している方へ
最後に、これから Next.js を採用しようとしている方が気になっているのではないかと思われるポイントについてお話ししていこうと思います。
Next.js を採用するにあたって最も気になるポイントはやはり Node.js の実行環境の運用コストではないでしょうか。 Next.js では一部の例外を除けば SSR を使用するか否かに関わらず実行には Node.js サーバーが必要となります。 SSR は必要としていないが Next.js の様々な恩恵を受けたいという開発者にとっては Node.js サーバーの運用コストが気になるところかと思います。
CLINICS では AWS ECS/Fargate で Next.js サーバーを運用しているのですが、Node.js の実行環境を運用するコストは確かに発生しているものの、現状ではそれほど大きな負担には感じていません。 API サーバーが別で存在する構成では Next.js サーバーが担うのはルーティングに従って API サーバーからデータを取得して SSR するといった比較的シンプルな役割になります。 そのため、データベースを扱うような本格的なバックエンドに比べると運用の手間は低く抑えられている印象です。
どうしても Node.js の実行環境を持たずに運用したいという場合には Static Exports というビルド時に HTML を含む静的アセットを出力する機能の利用も検討してみると良いかもしれません。 ただし、機能面でいくつかの制約があるので、採用を検討する際にはよく確認しておくことをお勧めします。 どちらかと言えば Static Exports は静的なサイトを構築するのに向いている機能であり、動的なコンテンツを扱うようなアプリケーションにはあまり向いていないというのが正直な感想です。 従来の SPA に多く見られるようなビルド済みの JavaScript, CSS などの静的アセットを S3 などのストレージに配置して HTML はバックエンドのサーバーから配信するといった構成が想定されていない点も注意が必要かと思います。
また、Next.js の採用にあたって App Router に追従すべきかというのも悩ましいポイントです。 App Router は Next.js 13 で導入された新しいルーティングシステムで、Server Components/Actions などの React の新機能もサポートしている点が魅力ですが、リリースから日が浅くまだしばらくは様子を見たいと考えている方も多いのではないでしょうか。
現状 CLINICS ではまだ App Router への移行は考えておらず実際の使用感などをお話しすることはできないのですが、ドキュメントを読む限りでは Pages Router の扱いにくい部分がうまく改善されている印象はあります。 ただ、Pages Router のルーティング機能と完全な互換性がない点が移行を考える上で最大のネックだと感じています。 こういった移行のコストや Next.js 自体が App Router への移行を推奨していることを踏まえると、今から新規に開発を始めるのであれば App Router で開発を始めてしまった方が無難な選択かもしれません。
総じて、Node.js サーバーの運用コストが許容できるか、App Router や Server Components/Actions に魅力を感じるかといった点が Next.js の採用を検討する上でのポイントになるのではないでしょうか。
まとめ
CLINICS では Next.js を採用しており、SSR と CSR を併用する形で比較的ライトに SSR を取り入れてフロントエンドの開発を進めています。 当初は SEO 面でのメリットを重視していましたが、実際に使ってみるとパフォーマンスの向上や開発体験の向上など様々なメリットを感じています。 セッション管理については課題を抱えている部分もありますが、今後はそういった部分を改善しより一層 SSR の恩恵を受けられるようにしていきたいと考えています。
Next.js の他にも Remix と React Router の統合が発表されるなど、さらに SSR を活用しやすい環境が整いつつあるという点でも、SSR の今後は非常に楽しみな部分が多いと感じています。