aumoにおけるwebサイト表示速度改善の取り組み

「速度改善プロジェクト第2段」 aumoの比較サイト表示速度改善の取り組みについて紹介します。

こんにちは、aumoに2022年に新卒入社したエンジニアのクリモトです。本記事では「速度改善プロジェクト第2段」 aumoの比較サイト表示速度改善の取り組みについて紹介します。

前回のプロジェクトであるaumoの記事サイトにおける速度改善の取り組みはこちらをご覧ください。

1. サイトの表示が速いとどんなメリットがあるのか?

サイトの読み込みが速いと以下のようなメリットがあるとGoogleが報告しています。

  • 広告などの売り上げが伸びる
  • ページの表示順位が上がる
参考

Core Web Vitals によるビジネス インパクト 
ページの読み込み速度をモバイル検索のランキング要素に使用します

このようにサイトの速度を改善することで、ビジネスに大きなインパクトを与えることができます。

また、サイトのレスポンス速度(≒表示速度)を改善することで、

  • 1リクエストに使われるサーバーのリソースが減り、サーバーの台数を減らせる
  • デバッグ時の待ち時間が減り、開発効率が上がる

といった効果も見込めます。

それでは、この重要なサイト表示速度改善のためにaumoで実際に行った施策を見ていきましょう!

2. バックエンドのレスポンス速度を改善する

aumoの比較サイトは、Rails × GraphQLを用いたバックエンドとNuxt.jsが動作するフロントエンドから構成されています。今回はまずバックエンドの改善から取り組みました。

2.1. まずはバックエンドサーバのレスポンス速度の推移をグラフ化する

今回の速度改善プロジェクトが始まった時点では、aumoの比較サイトのバックエンドにはレスポンス速度を収集する基盤が特にありませんでした。この状態でプロジェクトを進めても、施策の効果が分からないので、まずは

①サーバーのアクセスログを収集

②アクセスログを集計し、日毎のレスポンス速度の変化をグラフ化する 

ことから始めました。

アクセスログの集計には、「DatadogのようなAPMサービスを使う」か「NGINXのログを、fluentdで吸い上げてBigQueryにアップロードする」の2つが候補として上がりました。最終的には

  • BigQueryの方がAPMサービスより安く済みそう
  • APMサービスを用いてGraphQLのスキーマごとにメトリクスを取る方法がネット上にあまり無い
  • Nginx × Fluentd × BigQueryの構成はaumoで既に使ったことがあり、その際の知識が活かせる

ということで、「NGINXのログを、Fluentdで吸い上げてBigQueryにアップロードする」方法を採用しました。そして最後にBigQueryのデータをData Studioに取り込みグラフ化することで、以下のようにGraphQLのスキーマごとに日々のレスポンス速度の変化が一目でわかるようになりました。

Data Studioを自分は今回初めて使ったのですが、無料で表現力のあるグラフを簡単に作成できて凄いですね。簡単なメトリクスを計測したいだけなら、DataDogのようなAPMサービスは不要な気がします。

2.2. N+1を解消する

メトリクスが取れるようになったので、ここからは遅いクエリを1つ1つ潰していきます。GraphQLでN+1が大量に起きていることが、僕が入社する前から問題になっていたらしいので、まずは実際に1リクエストでどれくらいのクエリが発行されているのかを確認してみました(railsのSQLのボトルネックを探すには、rack-mini-profilerが便利です)。その結果が↓

まさかの1リクエストに1827個のクエリが発行されているという驚愕の結果に(予想より桁が1つ多かった)😵

N+1の対策としては、rack-mini-profilerを使うと、クエリがコードのどの部分で発行されているかが分かるので、N+1が発生している箇所を一つずつ地道に特定しては、ActiveRecordのincludesメソッドを使ってEager Loadingをする方法を取りました。その結果1リクエストあたりのクエリ発行数を52個まで削減することができました!

ただ以下のような多対多の関係にあるモデルをincludesすると、spotsテーブルからspot_photosテーブルを経由してphotosテーブルのレコードを1つだけ取得したい場合でも、spot_photosテーブルのインスタンスが、spotに紐づくphotoの数だけ生成され、紐づくphotoの数が増えれば増えるほどレスポンスが遅くなっていく問題が発生することが分かりました。N+1が発生していた時の方がむしろ高速に動作する事態になったため、一部のテーブルではあえてN+1を許容する戦略を取ることにしました(本当は非正規化されたテーブルを作成したりした方が良いのですが、そこまで劇的な速度改善は見込めなかったので、その方法は採用しませんでした)。

2.3. MySQLをElasticsearchに移行

N+1は解消されたものの、joinとsortが組み合わされていて1クエリに5秒程度かかってしまう以下のようなクエリがありました。

SELECT 
 articles.* 
FROM 
 articles 
INNER JOIN 
 article_keywords ON article_keywords.article_id = articles.id 
WHERE 
 articles.platform IN (0, 1) 
 AND article_keywords.keyword_id = 1 
ORDER BY 
 articles.published_at DESC 
LIMIT 6

こういったクエリは通常は非正規化したテーブルを作成して対処したりすると思うのですが、aumoでは全文検索エンジンとしてElasticsearchを使っており、ariticleのインデックスも既に作成済みだったので、Elasticsearchを非正規化されたテーブルの代わりに使用することにしました。

その結果5秒ほどかかっていたクエリを10ミリ秒まで短縮することができました。

2.4. サーバー台数を追加

これらの施策を全てやっても依然としてレスポンスがたまに遅くなることがありました。BigQueryに溜まっているログをレスポンスが遅い順にsortしてみると、遅いリクエストが来た時刻が固まっており、一時的に想定よりも多いアクセスがきてしまった結果、レスポンス速度が悪化しているようでした。そこでサーバーの台数を増設する対策をとることで、レスポンス速度が常に安定するようになりました。

2.5. バックエンドのチューニング結果

以上のサーバーサイド周りの改善を全てやった結果、今回修正を入れたGraphQLのクエリはどれも最初の1/2程度までレスポンスにかかる時間を減らすことができました🎉

またN+1が解消されDBへの負荷が減った影響で、DBインスタンスを一台減らすこともでき、インフラ費用が削減されました。

3. フロントエンド周りのチューニング

サーバーサイドの改善は一通り終わったので、次はフロントエンドの改善を行っていきます。フロントエンドのチューニングはGoogleが提供しているLighthouseが改善項目を色々と教えてくれるので、それを愚直に潰していく方針で進めました。

3.1. 不要なjavascriptを削減する

Lighthouseに「不要なJavaScriptがある」と警告が出ていたので、webpack-bundle-analyzerを使用して不要なJavaScriptが無いか見てみました。

すると、momentのlocaleが日本語以外の不要なlocaleまで取り込んでしまっており、これを改善するのが簡単に大きな効果を得られそうだったので、日本語のlocaleファイルのみを読み込むように変更しました。その結果localeの圧縮後のファイルサイズを47KB -> 701B まで減らすことができました。

※本当はmomentを自作の関数に置き換えてmoduleごと削除したかったのですが、momentがコードのいたる部分で使われており、それなりに時間がかかりそうだったので、今回はやりませんでした。他にもswiperというカルーセル用のmoduleもCSSで置き換えることで、かなりの量のファイルサイズを軽減出来そうだったのですが、こちらも時間がかかりそうだったので着手しませんでした。今後はこれらのmoduleは使わないようにしつつ、時間があるときに少しずつ置き換えていけると良いなと考えています。

その他にもwebpackのchunckの設定を変更するといったチューニングを行い、読み込まれるJavaScriptのファイルサイズを削減しました。

3.2. CSS、JavaScript、Imageの遅延読み込み

続いてLighthouseにCSS、JavaScript、Imageの遅延読み込みを提案されていたので、重要でないJavaScriptとCSSは全て遅延読み込みをさせるようにしました。

なお、画像を遅延読み込みする場合は、予め画像が表示される部分の領域を確保しておかないと、Lighthouseの指標の1つであるCLS(表示されるページコンテンツにおける予期しないレイアウトのずれ)が悪化するので注意が必要です。

3.3. サーバー台数を増やす

サーバーサイドと同じく、フロンエンドのサーバー台数も、リクエスト数に追い付いていない時間帯がありました。

原因としては、aumoの比較サイトではフロントエンドにNuxt.jsを採用しており、Nuxt.jsの基盤にあるNode.jsは、シングルプロセス・シングルスレッドで動いています。しかし、aumoではCPUコア数が多いインスタンスタイプを少ない台数で稼働させており、リソースを上手く使い切れていませんでした。そこで、コア数の少ないインスタンスタイプを多数動かす構成に変えることで、サーバー費用をそれ程増やさずにより多くのリクエストを捌けるようにしました。

成長過程にあるサービスを運営している場合は、リクエスト数も時間が経つごとにどんどん増えていくので定期的にサーバー台数やスペックを見直していく必要があるということを学べて良かったです。

3.4. フロントエンドのチューニング結果

速度改善プロジェクト前とプロジェクト後で比較すると、Lighthouseの指標を大きく改善することが出来ました。

また、今回のプロジェクト前は比較サイトの開発をする時は、コードの変更を確認するためにページをリロードすると30秒近く待たされて、非常に開発効率が悪かったのですが、今では待ち時間も10秒程度まで短縮され、開発効率もかなり良くなりました。

まとめ

今回の速度改善プロジェクトの結果、無事サイトの表示速度が改善され、インフラ費用を削減することもできました。サイトの速度が改善されたことでページの表示順位にもプラスの効果が現れることも期待はしていたのですが、現時点では、そのような効果は現れていません。Googleのアルゴリズムがサイトの表示順位を決める際に一番重視するのは、やはりサイトのコンテンツであり、表示速度はそこまで重視はしていないということなのでしょう。

新卒で入社してすぐに、前からやってみたかったwebアプリケーションのチューニングを体験させて頂けて、非常に成長できた気がしています。これからもこの調子で色々なことを経験していきたいです!

なお、aumoではwebサイトを0.1秒でも速くしたいエンジニアを募集しています!ご興味がある方は是非、弊社採用ページ からご応募ください!