😆

【Rails】パフォーマンス改善にすぐに役立つTips集

2021/02/05に公開1

はじめに

Railsにおけるパフォーマンス改善に役立つTipsを集めてみました。
すぐに使えるものから、少し改善に時間がかかるものまで幅広く集めています。

開発中のアプリのパフォーマンス改善のお役に立てれば幸いです。

【Tips1】 N+1を改善する

何はともあれN+1が発生していたら、それを解消するようにしましょう。
大抵の場合、そこがアプリのパフォーマンスのボトルネックになっているはずです。

books_controller.rb
class BooksController < ApplicationController
  def index
    @book = Book.all
  end
end
index.html.erb
<% @book.each do |book|
  <%= book.title %>
  <%= book.user.name %> # ここでN+1が起きている
<% end %>     

上のコード例だとbookの関連先のuserを読み込むところでN+1が起きています。

下記のようにコントローラーを書き換えましょう。

books_controller.rb
class BooksController < ApplicationController
  def index
    @book = Book.includes(:user)
  end
end

モデル.allで全てのモデルを取得してきている場所はN+1が起きている可能性が高くなり、大抵allは使わなくなることが多いです。
N+1を検知するためにgemのbulletをアプリへ導入するのもおすすめです。

https://github.com/flyerhzm/bullet

また、上の例だとincludesを使用していますが、preloadeager_loadを使い分けられるようになるといいですね。

ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い

【Tips2】 countではなくてsizeを使用する

countを使用するとSQLを発行してしまいます。
そのため、モデルの数などを調べたい時はsizeなどで代用しましょう。

意外とやってしまいがちなケースですが、countsizeへ置き換えるだけなので、手軽にできます。

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のsidekiqdelayed jobなどで重い処理を非同期にしてしまう。
メール送信処理を非同期にする方法をよく見かけます。

終わりに

いかがだったでしょうか。
以上、Railsでコーディングする上で意識するだけで簡単にパフォーマンス改善ができるTipsを紹介しました。

他にもこんなやり方あるよだったり、ここが間違っているよなどありましたらコメント欄でそっとお知らせください笑

Discussion

Hidden comment