LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

ISR化でIESHILの建物詳細ページの読み込み速度を10倍に改善した話

初めに

こんにちは、IESHILでエンジニアをしているgccjです。
IESHILはマンション査定価格がいますぐわかるサービスです。

2021年4月6日にその中でも一番アクセスされている建物詳細ページをISR(Incremental Static Regeneration)化しました。
それについて、目次の順番でご紹介していきたいと思います。

目次

  1. 前提--SST, CSR, SSR, SSGの説明
  2. 既存のシステム構成
  3. 既存のシステム構成における課題
  4. 建物詳細ページをISR化した理由
  5. 建物詳細ページをISR化した結果
  6. 建物詳細ページをISR化した後のシステム構成
  7. 最後に

前提--SST, CSR, SSR, SSGの説明

まず、「そもそもISRって何?」という説明はこの後の ISRをした理由で触れる予定です。
一方、それを理解するために、

  1. Server-Side Templating(以下、SST)
  2. Client-Side Rendering(以下、CSR)
  3. Server-Side Rendering(以下、SSR)
  4. Static Site Generating(以下、SSG)

についての理解が必要です。
Web系のエンジニアであればご存知の内容とは思いますが、超簡単な説明だけさせていただきます。

まず、ユーザーに表示するコンテンツ(完全なHTML)をレンダリングする場所によって、SSTとCSRに分けられます。

レンダリング方式 レンダリング場所 技術例 主なメリット 主なデメリット
SST サーバー Rails + ERB - SEOに有利 - インタラクティブなページに向いていない
CSR ウェブブラウザ React - インタラクティブなページに向いている - SEO不利
- レンダリング速度がクライアント側の端末性能に左右される

そして、SSRは厳密に言えば、SST+CSR(Universal Rendering)になりますが、
現在のところ、一般的にはサーバー側がCSRまで担当することを指すようです。

レンダリング方式 レンダリング場所 技術例 主なメリット 主なデメリット
SSR サーバー Next.js - SEOに有利
- インタラクティブなページに向いている
- TTFBが遅い
- 専用サーバーが必要

また、SSGは、サーバーサイドでレンダリングするのですが、
SST、CSR、SSRではクライアントからのリクエストごとにレンダリングするのに対し、
SSGは事前に全てのページをレンダリングします。
何回アクセスされても事前にレンダリングされたページを再利用して、クライアントに返します。
リクエストごとにレンダリングする必要がないため、ページスピードがとにかく速いです。
一方、事前に全てのページをレンダリングする必要があるため、下記の問題もあります。

  1. ページ数が多ければ多いほどレンダリングコストが大きくなります。
  2. データの更新が必要となる場合、全てのページを再レンダリングする必要があります
レンダリング方式 レンダリング場所 レンダリングタイミング
CSR
SST・SSR
ウェブブラウザ
サーバー
リクエストがある度
SSG サーバー 事前、通常WebAppをbuild時

*より詳しい説明はインターネット上にある記事をご参照ください。

既存のシステム構成

IESHIL自体はRuby on Rails(以下、Rails) 4系から進化し続けてきました。 今回建物詳細ページがISR化される前までの簡単なシステム構成は下記になります。

一言で言うと、 建物詳細ページを含む全てのページでSST(Rails + Slim)とCSR(React)が混在していました。Rails+Slim(サーバー側)でページのベースとなる部分をレンダリングした後、React(クライアント側)で残りのパーツをレンダリングする形となっていました。

既存のシステム構成における課題

この構成に伴う私たちが直面していた課題として、UX(ユーザー体験)観点とDX(開発者体験)観点とそれぞれ説明します。

UX観点

1. ページスピードが遅い

先ほども触れたとおり、SSTがベースになっているため、クライアントからのリクエストが都度サーバーに到達します。
その結果、TTFBがより遅くなります。
さらに、CSRが必要なため、トータルでユーザーにページが表示されるまで遅かったです。

2. CLS(Cumulative Layout Shift 画面のガタツキを表す指標)が高い

SSTでレンダリングされたベースの上でCSRを行うため、クライアント側で描画した後に画面のガタツキが多く(CLSが高い)発生していました。

3. 価値をユーザーに届けられない恐れ

CSRの際には必要なデータをWeb APIから取得していました。
そのため、クローラーが来た際に、クロールバジェット(単一のサイトに対するクローラーのクロール上限のような物)が消費されることになります(IESHILの場合70%ほど)。
いわゆる、CSRで発生するデータ通信がクローラーのIndex率を低下させています。
その結果、本来ユーザーに提供できる価値が検索結果に出てこない恐れがあります。

DX観点

1. 開発コストが高い

一つのページに、SSTのパーツとCSRのパーツが混在していました。
一つのユーザー価値を提供するために、この辺りがRailsであの辺りがReactで、といったように二つのアプリを修正する作業が発生しています。
例えば、パーツ表示順番の切り替えだけでも、パーツの粒度により、修正が大変な時がありました。

2. ウェブブラウザごとの差分への対応

CSRの場合、クライアント側が利用するウェブブラウザで無事に実行できるように担保する必要があります。
また、CSRの部分が多いほど、対応コストが増えていくことになります。
現時点、IESHILユーザーの中で、IE11の利用がまた一部があるため、対応が求められます。

3. 開発者のモチベーション

エンジニアとして、何か新しい仕組みや技術を取り入れれば、モチベーションがあがりますので、常に新しいことをチャレンジしたいです。

建物詳細ページをISR化した理由

上述した課題を解決するため、私たちは色々な対策を探りました。 結果としてIncremental Static Regeneration(増分静的再生、以下はISRと略)にシフトすることにしました。

ISRにシフトしたい理由

ISRはNext.js 9.5より正式リリースされたSSRをベースとした仕組みです。

SSGと同じく、レンダリングしたページを再利用するのですが、
一定期間でSSRで再レンダリングすることでデータの更新性を担保しつつ、SSGのような事前に大量のページのレンダリングを必要としないことが特徴です。

レンダリング方式 レンダリング場所 レンダリングタイミング 技術例
ISR サーバー 一定期間内の初めてのリクエスト Next.js

話をもう少しわかりやすくするために、この一定期間をたとえば60sとして、IESHILビルという建物詳細ページに対するリクエストがあったときのサーバー側の処理を分解して見てみましょう。

No. リクエストタイミング 処理 備考
1. 初めてのリクエスト IESHILビルのページ(v1)をレンダリングする - この時点まではSSRと同等
- 説明するため、tag v1を付ける
2. 初めてのリクエスト IESHILビルのページ(v1)をCacheする
3. 初めてのリクエスト CacheされたIESHILビルのページ(v1)を返す
4. その以後の60s以内に来たリクエスト 3と同じCacheされたIESHILビルのページ(v1)を返す
5. 60sに来たリクエスト CacheされたIESHILビルのページ(v1)を返す 3と同じ
6. 60sに来たリクエスト IESHILビルのページ(v2)を新しくレンダリングする - 説明するため、tag v2を付ける
7. 60sに来たリクエスト IESHILビルのページ(v2)をCacheする
8. 60s以降次の60s以内にリクエスト CacheされたIESHILビルのページ(v2)を返す

仕組みの要件として、一定期間内で下記の2つを実行する必要がありました。

  1. 一回レンダリング(SSR)したものをCacheして再利用すること。
  2. バックグラウンドでCacheを更新すること。

気付きましたか?
実はこれがstale-while-revalidateというキャッシュ戦略と同じ考え方です。
ゆえに、SSRできるフレームワークとstale-while-revalidateをサポートしているCDNを使えば、Next.js以外の技術でもISRを実現可能です。
また、ISRを実装する場合 Vercel を使うのが一般的ですが、IESHILでは HerokuとFastly(stale-while-revalidate対応)を使っていたため、今回の実装もHerokuとFastlyを利用しました。

それではこの仕組みはどのように私たちの課題解消できるのかを説明します。
同じくUXとDXの観点から説明します。

UX観点

1. ページスピードを高速化

(一定期間内で)Cacheをそのまま返すため、都度SSRするコストが防げます。
また、都度SSRが必要ないため、CDNを利用すれば、(CDNのエッジ)サーバーとクライアントとの物理的距離を短くすることもできます。
そのため、TTFBを大幅に減らせます。

2. CLSを低減

個人情報などセンシティブな情報以外、ほぼ全てのコンテンツがSSRされるため、CLSが最小限に抑えられます。
センシティブな情報が入っていないページ(IESHILで例えば、未ログインの建物詳細ページとか)の場合、CLSを0.000にすることができます。

3. より多くの価値を届ける可能に

個人情報などセンシティブな情報以外、ほぼ全てのコンテンツがSSRされるため、CSRでAPIから取る必要なデータが大幅に減らせます。
結果、それによるクロールバジェットの消費も抑えられ、クローラーのIndex率を向上させ、より多くのIEHSILページが関連の検索結果に出るようになります。

DX観点

1. 開発コストを削減

SSRにすることで、全部Js(今回の場合、Next.jsになる)に統一されます。
最終的に、一つのユーザー価値を提供するため、二つのアプリを開発するようなことがなくなります。

2. ウェブブラウザごとの差分への要対応箇所を削減

ほぼ全てのコンテンツがSSRされるため、CSRの割合を縮小することができます。
それによって、ウェブブラウザ対応もより楽になります。

3. 開発者のモチベーションを向上

ISRと言う新しい仕組み、Next.jsもチーム内初採用、ウェブブラウザ対応が楽になるなどなど、全て開発者のモチベーションにプラスになります。

建物詳細ページにする理由

上でご紹介したようにISRにすることでUXとDXの両方に良い影響があります。
サイト全体をISR化できれば、もちろん一番望ましいかもしれません。
一方、それを実現するのに、エンジニアリソースが必要である上に、開発時間も伸びざるを得ないことになります。
なので、私たちは大きな課題を分割し、成果を継続して出していくことを選びました。
その結果として、一番アクセスされている、IESHILがサービスとして提供しているメイン価値でもある建物詳細ページをISR化しました。

建物詳細ページをISR化した結果

今回建物詳細ページをISR化した後、周囲に「え〜!ページを開くのを感じられないぐらい速いなあ!」といった声をたくさん貰いました。
定性的な指標として、嬉しく思っています。
一方、建物詳細ページの定量的な数値も見てみましょう。
*モバイルでの指標

Google Serach Consoleのページ エクスペリエンス指標

その他の指標

指標 Before After
TTFB 約数百ms前後 約十ms前後
LCP 約数秒前後 約0.数秒前後
CLS 約0.500前後 約0.000
優良URL件数 約16倍UP
良好URL表示回数/日 約2倍UP
Lighthouseのパフォーマンススコア 40~50程度 70~80程度

*2021/04/30時点のデータ。改善やISR移行に伴う数値が変更する場合がある。

その他直帰率の削減や滞在時間の増加など滞在指標の改善にも明確な良い影響を与えています。

建物詳細ページをISR化した後のシステム構成

それでは建物詳細ページISRの実現を紹介します。

ISRを実現したシステム構成図

動作説明

また、IESHILビルの詳細ページを例とします。
IESHILビルの詳細ページがレンダリングされてからユーザーに表示するまでの流れで説明します。

  1. Next.js AppはRails AppからIESHILビルのデータを取得し、IESHILビルの詳細ページ(v1)をレンダリングします。
  2. Next.js AppはIESHILビルの詳細ページ(v1)のSurrogate-Controlヘッダーにmax-agestale-while-revalidateを設定します。
    1. max-ageの役割:IESHILビルの詳細ページ(v1)が再利用できる(Cacheされる)最大時間を規定します。
    2. stale-while-revalidateの役割:max-age経過後、バックグラウンドでCacheが更新されるまでIESHILビルの詳細ページ(v1)が再利用できる最大時間を規定します。
  3. Next.js APPはSurrogate-Controlヘッダーが設定されたIESHILビルの詳細ページ(v1)をFastlyに返します。
  4. FastlyはSurrogate-Controlヘッダーで指定した時間を基準として、IESHILビルの詳細ページ(v1)をCacheし、ユーザーに返します。
    1. IESHILビルの詳細ページ(v1)をCacheしてからmax-age時間内の場合
      1. IESHILビルの詳細ページ(v1)をユーザーに返します。
    2. IESHILビルの詳細ページ(v1)をCacheしてからmax-ageを経過かつ、stale-while-revalidate時間内の場合
      1. IESHILビルの詳細ページ(v1)をユーザーに返します。
      2. 同時にNext.js AppへIESHILビルの詳細ページ(v2)を新しくリクエストします。1~4が実行されます。
備考

今回ISRされたのは建物詳細ページになります。
建物詳細ページ以外のページは最初はFastly経由になっていますが、Fastly上での処理がなく、その後の処理的に既存のシステム構成に示した物と全く同じです。

最後に

今回はIESHILで一番アクセスされている建物詳細ページをISR化したことを紹介しました。
技術上のより詳細な話や、ISRにおけるA/Bテストなどについて、また別の記事で紹介する予定です。
気になる方はご期待ください!