Railsで主要ページのパフォーマンス改善を行った話

こんにちは。BUYMA TRAVELのWebエンジニアの赤間です!

BUYMA TRAVELは BUYMA を運営する株式会社エニグモのグループサービスです。

1. はじめに

弊社では日本語対応のオプショナルツアー・貸切ガイド予約サイト’’BUYMA TRAVEL’’を開発・運営しています。

その中でも「エリアページ」と「スポットページ」は検索エンジンから流入したユーザが最初に訪れることの多い主要なページです。

ここ数ヶ月でユーザにとってより使いやすいページを目標に、クチコミやおすすめ商品の掲載など様々な要素・機能の追加を行ってきました。しかしその一方で表示速度が低下し、商品数の多いページによっては表示完了まで最大3秒かかってしまう状況でした。

そこでこれらのページを対象に、Railsアプリケーションのパフォーマンス改善を実施しました。本記事では調査の流れから改善内容、その改善を通して得られた知見をご紹介します。

2. エリアページ・スポットページとは

ここで簡単にサービス紹介もかねて、この2つのページのご紹介です。

エリアページ

https://travel.buyma.com/service/a040118/

その名の通り、エリア(地域)単位で商品をまとめている特設ページです。

スポットページ

https://travel.buyma.com/service/a040118/s0000001/

こちらは、観光スポット単位で商品をまとめている特設ページです。

どちらのページも人気商品やプライベート商品、クチコミ、おすすめスポットなどのコンテンツを掲載しています。 一見すると単純な一覧ページですが、実際にはユーザに新鮮な情報を提供するための複数の検索処理や集計処理、共通コンポーネントによって構成されており、システム的に比較的処理の多い重いページとなっています。

3. なぜ改善が必要だったのか

これらのページは検索エンジンから’’BUYMA TRAVEL’’に流入したユーザが最初に目にするページです。

そのため表示速度の低下は単なる技術的な課題ではなく、サービス全体の利用率や売り上げにも直結する可能性があります。 また、エリアページとスポットページはサービス内でもアクセス数が多く、改善効果が大きいページでもありました。

実際に計測したところ、改善前ではページ表示までに最大3秒程度かかってしまうケースも確認できました。 ページ表示速度と離脱率には相関があると言われており、ユーザがページの表示を待っている間に離脱してしまえば、どんなに魅力のある商品を掲載していても見てもらうことはできません。

そこで、本格的なパフォーマンス改善に取り組むこととなりました。

4. AIを活用したボトルネック調査

エリアページは長年運用されてきたページであり、コードベースも大規模化していました。

そのため単純に「とりあえずSQLを削減する」といった場当たり的な改善ではなく、本当にボトルネックとなっている処理を特定する必要がありました。 Railsアプリケーションではコントローラ・モデル・ビュー(・ヘルパー)に処理が分散しているため、どこで処理時間を消費しているかを把握するだけでも手間がかかります。

今回はDatadogで遅いトランザクションを特定し、Cursorを利用してコードベースの調査を行いました。

AIに「TTFBを悪化させている箇所を探して欲しい」 という依頼を行い、懸念箇所や修正候補をリストアップしてもらいました。

5. 調査して見つかった問題箇所

分析の結果、大きく以下の3つの課題が見つかりました。

  • SQLの発行数が多い
  • 同一リクエスト内で重複した計算が発生している
  • キャッシュが十分に活用できていない

特にSQL関連の問題が深刻で、全体処理時間の約60%を占めている有り様でした。

6. 実施した改善

SQL発行数の削減

調査の結果、レスポンス時間の大部分をデータベースアクセスが占めていることがわかりました。

特に商品一覧の表示処理では、関連データを取得する際にN+1問題が発生している箇所が複数存在していました。

N+1問題とは、一覧取得後に各レコードごとに追加のSQLが発行されてしまう問題です。 たとえば商品を10件まとめて表示する場合、本来1回で済むはずが11回以上のDBアクセスが必要になることがあります。 さらにデータ量が増えるほどSQL実行回数も増加するため、パフォーマンスへ大きな影響を与えてしまうのです。

今回の改善では includes を利用したEager Loadingを積極的に導入し、必要な関連データを事前にまとめて取得するように変更しました。 また、同じ情報を異なるコンポーネントで個別に取得している箇所も存在していたため、取得処理を集約し不要なクエリ発行を削減しました。

結果として、SQL発行数を大幅に削減でき、レスポンス改善に最も大きく貢献した施策となりました。

同一リクエストで同じ計算をしていた

調査を進める中で、同一リクエスト内で何度も同じ計算を行なっている箇所も見つかりました。これは本当にもったい無いですね、、、

単体で見ると小さな処理ですが、ページ全体では何度も呼び出されるため無視できないコストになります。 そこでメモ化を導入し、一度計算した結果を再利用するように変更しました。 また、一部の集計処理にはキャッシュを活用することで再計算そのものを削減しています。

大きな改善ではありませんが、細かな最適化の積み重ねが最終的なレスポンス改善につながりました。

お気に入り機能とキャッシュの見直し

今回の改善で特に興味深い問題が、お気に入り機能とキャッシュの関係でした。 商品カードはページ内で大量に表示されるため、本来であればカード単位でキャッシュしたいコンテンツです。商品名や価格、評価、パートナー情報などは頻繁には変化しないため、一度生成したHTMLを再利用できることで大きな効果が期待できます。

しかし、お気に入りの状態はユーザごとに異なる情報です。 同じ商品であってもあるユーザにとっては「お気に入り登録済」、別ユーザにとっては「未登録」という状態があります。

そのため商品カード全体をキャッシュしてしまうと、

  • ユーザごとに異なるキャッシュを生成する必要がある
  • キャッシュキーが複雑になる
  • キャッシュヒット率が低下する という問題が発生します。

実際に、改善前はこの問題を回避するために、お気に入りボタンを除いた「パートナー・評価」「商品情報」の2つの領域に分けて個別にキャッシュしていました。

この構成でもキャッシュがない状態より圧倒的に効率的ですが、キャッシュの管理が複雑であったり、テンプレートも読みづらい実装になっていました。

そこで今回の修正では、商品カードとお気に入りの状態を分離し、商品カード全体を共通キャッシュできる構成へ変更しました。 商品カード本体は全ユーザ共通のHTMLとしてキャッシュし、商品IDごとのお気に入りの状態は別で取得する形です。 これにより商品カード自体は1つのキャッシュとして扱えるようになり、キャッシュ構成をシンプルにしつつ、高いキャッシュ効率を実現しています。

結果としてレスポンス速度だけでなく、今後の保守性や機能追加のしやすさという点でも大きな効果を得ることができました。

7. 改善結果

改善前ではページ表示までに最大3秒程度かかる状態でした。 改善後は1秒に満たない時間で表示できるケースが大半となり、体感速度も大きく向上しました。

今回の改善では特定の1箇所を直したのではなく、

  • SQLの最適化
  • 重複処理の削減
  • キャッシュの見直し といった複数の小さな改善を積み重ねたことが、この結果につながりました。

8. まとめ

当サイトの主要ページである、エリアページ・スポットページの速度改善についてご紹介しました。

パフォーマンス改善というと大規模なアーキテクチャ変更をイメージするかと思います。私自身はそのような印象を持っていました。

しかし実際には特別な技術ではなく、N+1の解消やメモ化、キャッシュ設計の見直しといった、比較的小さな改善の積み重ねでも、十分な効果が出せると実感しました。

Railsには優秀なキャッシュ機構や様々な最適化手法がありますが、まずは基本的なSQLの見直しが重要です。 普段のレビューでもN+1の有無や効率的なデータの取得については確認していますが、改めて見直してみると、どうしても漏れは出てくるものだと感じました。機能追加を重ねてきたページほど、定期的にパフォーマンスの棚卸しは必要だと思います。

今回で大きくレスポンスを改善できましたが、まだ課題は残っています。 特にレビュー集計処理にはサマリーテーブル導入の余地がありますし、ページビューカウントの非同期化など、更なる高速化が可能です。

今後もユーザ体験向上のため、継続的な改善を続けていきます。