【Rails】パフォーマンス改善にすぐに役立つTips集
はじめに
Railsにおけるパフォーマンス改善に役立つTipsを集めてみました。
すぐに使えるものから、少し改善に時間がかかるものまで幅広く集めています。
開発中のアプリのパフォーマンス改善のお役に立てれば幸いです。
【Tips1】 N+1を改善する
何はともあれN+1が発生していたら、それを解消するようにしましょう。
大抵の場合、そこがアプリのパフォーマンスのボトルネックになっているはずです。
class BooksController < ApplicationController
def index
@book = Book.all
end
end
<% @book.each do |book|
<%= book.title %>
<%= book.user.name %> # ここでN+1が起きている
<% end %>
上のコード例だとbookの関連先のuserを読み込むところでN+1が起きています。
下記のようにコントローラーを書き換えましょう。
class BooksController < ApplicationController
def index
@book = Book.includes(:user)
end
end
モデル.all
で全てのモデルを取得してきている場所はN+1が起きている可能性が高くなり、大抵allは使わなくなることが多いです。
N+1を検知するためにgemのbullet
をアプリへ導入するのもおすすめです。
また、上の例だとincludes
を使用していますが、preload
とeager_load
を使い分けられるようになるといいですね。
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い
【Tips2】 countではなくてsizeを使用する
countを使用するとSQLを発行してしまいます。
そのため、モデルの数などを調べたい時はsize
などで代用しましょう。
意外とやってしまいがちなケースですが、count
をsize
へ置き換えるだけなので、手軽にできます。
countを使用した場合
user = User.all
user.count
# count関数を用いたSELECT文が発行されてしまう
(4.6ms) SELECT COUNT(*) FROM `users`
=> 100
sizeを使用した場合
user = User.all
user.size
# SQLクエリは発行されない
=> 100
【Tips3】exist?の使用を控える
count
と同じくexist?
はモデルオブジェクトに使用すると、sqlを発行してしまいます。
存在するかを確認したい場合などはpresent?
などで代用しましょう。
exsit?を使用した場合
user = User.where(deleted: true)
user.exist?
# SQLが発行される
User Load (5.4ms) SELECT `users`.* FROM `users` WHERE `users`.`deleted` = TRUE
=> []
present?を使用した場合
user = User.where(deleted: true)
user.present?
# SQLは発行されない
=> []
【Tips4】allで取得した結果をeachで回さない
全ユーザーをall
で取得してそれをeachで回す・・・のようなパターンです。
バッチ処理とかにありがちなパターンですね。
User.all.each do |user|
# 何かuserのオブジェクトを使用して処理をするコード
end
N+1のTipsでも触れましたが、にallが処理に入ってきたときは一度そのコードを疑ってかかりましょう。
上述の実装だとUserの全件をメモリに展開してから、ひとつひとつの処理をeachで行っていくため、メモリ消費が激しくなります。
allのかわりにfind_eachを使いましょう。
User.find_each do |user|
# 何かuserのオブジェクトを使用して処理をするコード
end
find_eachはレコードを1000件取得ずつ取得し、その取得したレコードを1件ずつ処理してきてくれます。
1000件取得し終わったらまた、次の1000件を取得してきて・・・の繰り返しとなります。
ちなみにレコード展開数を指定したかったらその姉妹メソッドであるfind_in_batchesを使用しましょう。メソッドの引数で、レコード数を指定できます。
【Tips5】不必要なActive Recordオブジェクトの生成
上述のN+1問題や、eachパターンほどではないですが、これも気をつけないとやってしまいがちな実装となります。
user_names = User.all.map(&:name)
上述のmapを使用したやり方は不必要なActive Recordオブジェクトを生成してしまい、パフォーマンスがあまりよくありません。
Active Recordオブジェクトは膨大な数のモジュールやメソッドをラップしているので、生成コストが高く、それだけでメモリを費やしてしまいます。
Active Recordオブジェクトを生成しなくてもよいやり方があるならば、そちらのやり方を考えましょう。
user_names = User.pluck(:name)
pluckを使用することで、不要なオブジェクト生成を回避できました。
【Tips6】 キャッシュや非同期処理の導入を検討する
パフォーマンス悪化箇所にはキャッシュを使ったり、処理を非同期にする方法もあります。
キャッシュを使う
Redisなどの導入を検討する。マスター系のデータ読み込みなどに検討してみるといいかも。
ただ、導入箇所をよく検討しないとバグの原因になりがちです。
処理を非同期にする
gemのsidekiq
やdelayed job
などで重い処理を非同期にしてしまう。
メール送信処理を非同期にする方法をよく見かけます。
終わりに
いかがだったでしょうか。
以上、Railsでコーディングする上で意識するだけで簡単にパフォーマンス改善ができるTipsを紹介しました。
他にもこんなやり方あるよだったり、ここが間違っているよなどありましたらコメント欄でそっとお知らせください笑
Discussion