第24章|今さら学ぶ「パフォーマンス」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- 「遅い」ときの調査手順 — 症状の場所を特定してから治療する
- DBクエリの最適化 — N+1問題の復習と
explainの使い方 - counter_cache — カウントを事前に保存しておく
- キャッシング — よく出る料理は作り置きする
- フラグメントキャッシュ / ロシアンドールキャッシュ
- Solid Cache — Rails 8.0 標準のキャッシュストア
🏠 たとえ話で掴む「パフォーマンス」
パフォーマンス改善は 体の不調を治す のと同じです。
「なんか体がだるい」と言われても、医者はいきなり薬を出しません。まず「どこが痛い?」と聞いて、血液検査やレントゲンで原因を特定してから治療します。
「アプリが遅い」→ どこが遅い?
① DBが遅い? → SQLの実行時間を確認(ログ / explain)
② コントローラ?→ 不要な処理をしていないか確認
③ ビューが遅い?→ Partialのループが多すぎないか
④ ネットワーク?→ アセットのサイズが大きすぎないか
最も多い原因はDBクエリ です。まずはログでSQLの実行時間を確認するのが定石です。
⚡ パフォーマンス最適化とは何か — 技術的な定義
パフォーマンス最適化 とは、アプリケーションのレスポンス時間やリソース消費量を改善するプロセスです。
Webアプリのレスポンスは、大きく4つの区間に分解できます。
リクエスト → [① ルーティング] → [② コントローラ処理] → [③ ビュー描画] → レスポンス
↓
[④ DB問い合わせ]
Railsの開発ログでは、各区間の所要時間が表示されます。
Completed 200 OK in 850ms (Views: 120.5ms | ActiveRecord: 712.3ms)
この例だと、全体850msのうちDBに712ms(約84%)かかっています。 ボトルネック (一番時間がかかっている箇所)を特定してから対策するのが鉄則です。
パフォーマンス最適化で重要なのは、 推測ではなく計測に基づいて判断する ことです。「なんとなく遅そう」で最適化を始めると、効果の薄い場所に時間を使ってしまいます。
🔍 遅い箇所の特定
ログで確認
Completed 200 OK in 850ms (Views: 120.5ms | ActiveRecord: 712.3ms)
↑ ここが大きい = DB が遅い
ActiveRecord: 712.3ms なら、DBクエリに時間がかかっています。
rack-mini-profiler で可視化
# Gemfile
group :development do
gem "rack-mini-profiler"
end
ページの左上にリクエストの所要時間が表示され、クリックするとSQLの詳細が見えます。
bullet でN+1を自動検出
# Gemfile
group :development do
gem "bullet"
end
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true # ブラウザにアラート表示
Bullet.bullet_logger = true # ログに出力
Bullet.rails_logger = true
end
bullet は、N+1問題が発生したときにブラウザ上にアラートを表示してくれるGemです。「どのアソシエーションで includes が必要か」まで教えてくれるので、N+1対策が格段に楽になります。
⚡ DBクエリの最適化
N+1問題の対策(復習)(→ 第17章で詳しく扱います)
# ❌ N+1
Article.all.each { |a| puts a.user.name }
# ✅ includes で一括取得
Article.includes(:user).all.each { |a| puts a.user.name }
explain でクエリの実行計画を見る
Article.where(status: :published).explain
# => EXPLAIN for:
# SELECT "articles".* FROM "articles" WHERE "articles"."status" = 1
# Seq Scan on articles (cost=0.00..18.50 rows=500 width=200)
# ↑ Seq Scan = 全件スキャン(インデックスが使われていない)
Seq Scan(Sequential Scan)が表示されたら、 インデックスを追加すべきサイン です。
# インデックスを追加
add_index :articles, :status
# → Seq Scan が Index Scan に変わり、検索が高速になる
select で必要なカラムだけ取得
# ❌ 全カラム取得(bodyが大きいと無駄)
Article.all
# ✅ 必要なカラムだけ取得
Article.select(:id, :title, :user_id, :created_at)
counter_cache — カウントを事前に保存する
記事の一覧画面で article.comments.count を呼ぶと、記事ごとに SELECT COUNT(*) FROM comments WHERE ... が実行されます。記事が100件あれば100回のSQLです。
counter_cache は、関連レコードの件数を 親テーブルのカラムに事前保存 しておく仕組みです。
# マイグレーション:articles テーブルにカウント用カラムを追加
add_column :articles, :comments_count, :integer, default: 0, null: false
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :article, counter_cache: true
# ↑ counter_cache: true を付けると、
# commentが作成/削除されたとき、自動で articles.comments_count が更新される
end
# counter_cache なし(毎回SQL)
article.comments.count # → SELECT COUNT(*) FROM comments ...
# counter_cache あり(カラムを読むだけ)
article.comments_count # → SQLなし。articlesテーブルの値を返すだけ
一覧画面で件数を表示する場面では、counter_cache があるだけでクエリ数が劇的に減ります。
⚠️ ポリモーフィック関連では counter_cache は標準では使えません。
belongs_to :likeable, polymorphic: trueのように関連先が動的に決まる場合、Railsはどのテーブルのカウントカラムを更新すべきか判断できないためです。ポリモーフィックなカウントが必要な場合は、after_create_commit/after_destroy_commitコールバックで手動管理するか、counter_culture gem の利用を検討します。
🍱 キャッシング — よく出る料理は作り置きする
キャッシュ は、「一度作った結果を保存しておき、次回以降はDBに問い合わせず保存した結果を返す」仕組みです。レストランでいう 作り置き です。
フラグメントキャッシュ
ビューの 一部分(フラグメント) をキャッシュします。
<%# app/views/articles/_article.html.erb %>
<% cache article do %>
<%# ↑ この article の内容が変わるまでキャッシュされる %>
<div class="p-4 border rounded mb-4">
<h2><%= link_to article.title, article_path(article) %></h2>
<p>by <%= article.user.name %> · <%= article.created_at.strftime("%Y/%m/%d") %></p>
<span>❤️ <%= article.likes_count %></span>
</div>
<% end %>
cache article と書くだけで、Railsは article の updated_at をキーにしてキャッシュを管理します。記事が更新されると自動でキャッシュが無効化されます。
ロシアンドールキャッシュ
キャッシュの中にキャッシュを入れ子にするパターンです。
<%# 記事一覧のキャッシュ(外側) %>
<% cache @articles do %>
<% @articles.each do |article| %>
<%# 各記事のキャッシュ(内側) %>
<% cache article do %>
<%= render article %>
<% end %>
<% end %>
<% end %>
1件の記事が更新されても、 その記事のキャッシュだけが再生成 され、他の記事はキャッシュが効いたままです。
Solid Cache(Rails 8.0 標準)
Rails 8.0 では、キャッシュの保存先として Solid Cache がデフォルトです。従来のRedisの代わりに、 DBにキャッシュを保存 します(開発環境ではSQLite、本番環境ではPostgreSQL等のDBを使用)。
# config/environments/production.rb
config.cache_store = :solid_cache_store
# → Redis不要!DBだけでキャッシュが動く
| 従来(Redis) | Solid Cache(Rails 8.0) | |
|---|---|---|
| 追加ミドルウェア | 必要(Redis サーバ) | 不要(DBに保存) |
| 運用コスト | Redisの管理が必要 | DB だけで完結 |
| パフォーマンス | 高速 | 十分に高速(SSD前提) |
🛠️ KnowledgeNoteでの具体例
# app/controllers/articles_controller.rb
def index
@articles = Article.published
.recent
.includes(:user, :tags) # N+1対策
.select(:id, :title, :user_id, :status, :created_at, :likes_count)
.page(params[:page])
.per(20)
end
<%# app/views/articles/index.html.erb %>
<% @articles.each do |article| %>
<% cache article do %>
<%= render "article", article: article %>
<% end %>
<% end %>
<%= paginate @articles %>
💼 面接で聞かれたら?
Q:アプリが遅いとき、どう調査しますか?
「まず開発ログでレスポンスの所要時間を確認し、DBクエリ・ビュー描画・コントローラのどこに時間がかかっているか切り分けます。最も多い原因はDBクエリで、N+1問題やインデックス不足が該当します。N+1はincludesで解決し、インデックス不足はexplainで確認して追加します。一覧画面での件数表示にはcounter_cacheが有効です。ビューが遅い場合はフラグメントキャッシュを検討します。」
深掘りされたら:
- 「キャッシュの無効化はどうする?」→ Railsのフラグメントキャッシュはモデルの
updated_atをキーにしているので、レコードが更新されると自動で無効化される。- 「Solid Cache とは?」→ Rails 8.0標準のキャッシュストア。Redisの代わりにDBにキャッシュを保存するため、追加のミドルウェアが不要。
- 「counter_cache とは?」→ 関連レコードの件数を親テーブルのカラムに事前保存する仕組み。
comments.countのように毎回SQLでCOUNTするのを避けられる。ただし通常の has_many/belongs_to にのみ対応しており、ポリモーフィック関連には使えない。
🔗 もっと深く知りたい人へ(1次情報リンク)
- Rails ガイド:Rails のキャッシュ機構 — フラグメントキャッシュ / ロシアンドール
- Solid Cache(GitHub) — Rails 8.0 標準のキャッシュストア
- rack-mini-profiler(GitHub) — パフォーマンス可視化ツール
- bullet(GitHub) — N+1問題の自動検出ツール
まとめ
- ✅ 「遅い」ときはまず どこが遅いか をログで特定する。推測ではなく計測に基づいて判断する
- ✅ 最も多い原因はDBクエリ。N+1問題とインデックス不足を疑う
- ✅
explainでクエリの実行計画を確認。Seq Scan はインデックス追加のサイン - ✅ counter_cache で件数表示のクエリを削減。一覧画面の高速化に効果大
- ✅ フラグメントキャッシュでビュー描画を高速化。
updated_atで自動無効化 - ✅ Rails 8.0 の Solid Cache はRedis不要。DBだけでキャッシュが動く
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに