[RUBY] [Rails] A collection of tips that are immediately useful for improving performance

Introduction

Here are some tips to help you improve your performance in Rails. We have a wide range of items, from those that are ready to use to those that take some time to improve.

We hope it helps improve the performance of the app you are developing.

[Tips 1] Improve N + 1

If N + 1 is occurring anyway, try to eliminate it. In most cases, that should be the bottleneck for your app's performance.

books_controller.rb


class BooksController < ApplicationController
  def index
    @book = Book.all
  end
end

erb:index.html.erb


<% @book.each do |book|
  <%= book.title %>
  <%= book.user.name %> #Here N+1 is happening
<% end %>     

In the above code example, N + 1 is occurring when reading the user associated with the book.

Let's rewrite the controller as follows.

books_controller.rb


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

There is a high possibility that N + 1 is occurring in the place where all models have been acquired in model.all, and all is often not used. It is also recommended to install the gem bullet in the app to detect N + 1.

https://github.com/flyerhzm/bullet

Also, in the above example, ʻincludes is used, but it would be nice to be able to use preload and ʻeager_load properly.

Differences between ActiveRecord joins, preloads, includes and eager_load

[Tips 2] Use size instead of count

If you use count, you will issue SQL. Therefore, if you want to check the number of models, use size instead.

It's a case that tends to happen unexpectedly, but it's easy because you just replace count with size.

** When using count **


user = User.all
user.count
#A SELECT statement using the count function is issued
   (4.6ms)  SELECT COUNT(*) FROM `users`
=> 100

** When using size **

user = User.all
user.size
#SQL query is not issued
=> 100

[Tips 3] Refrain from using exist?

Like count, ʻexist?will issue sql if used on a model object. If you want to check if it exists, usepresent?` Instead.

*** When using exit? ***


user = User.where(deleted: true)
user.exist?
#SQL is issued
  User Load (5.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`deleted` = TRUE
=> []

*** When using present? ***

user = User.where(deleted: true)
user.present?
#SQL is not issued
=> []

[Tips 4] Do not turn the result obtained by all with each

It's a pattern like getting all users with ʻall` and turning it with each. It's a pattern that is common in batch processing.

User.all.each do |user|
  #Code that does something using the user's object
end

As I mentioned in the N + 1 tips, once all comes into the process, suspect the code.

In the above implementation, after expanding all User cases to memory, each process is performed by each, so memory consumption becomes heavy.

Use find_each instead of all.

User.find_each do |user|
  #Code that does something using the user's object
end

find_each fetches 1000 records at a time and processes the retrieved records one by one.

After acquiring 1000 items, the next 1000 items will be acquired again, and so on.

By the way, if you want to specify the number of record expansions, use its sister method, find_in_batches. You can specify the number of records in the method argument.

[Tips 5] Generating unnecessary Active Record objects

Although not as much as the N + 1 problem and each pattern mentioned above, this is also an implementation that tends to be done if you are not careful.

user_names = User.all.map(&:name)

The map-based approach described above creates unnecessary Active Record objects and does not perform very well.

Since Active Record objects wrap a huge number of modules and methods, they are expensive to generate and consume memory on their own.

If there's a way you don't have to create an Active Record object, think about it.

user_names = User.pluck(:name)

By using pluck, we were able to avoid unnecessary object creation.

[Tips 6] Consider introducing cache and asynchronous processing

You can also use the cache or make the process asynchronous in the place where the performance deteriorates.

** Use cache ** Consider introducing Redis, etc. It might be a good idea to consider reading master data. However, if you do not carefully consider the introduction location, it tends to cause bugs.

** Make processing asynchronous ** Gem's sidekiq and delayed job make heavy processing asynchronous. I often see how to make the mail sending process asynchronous.

At the end

How was that. So far, we have introduced tips that can easily improve performance just by being aware of it when coding with Rails.

If there are other ways to do this, or if there is something wrong here, please let me know in the comments section lol

Recommended Posts

[Rails] A collection of tips that are immediately useful for improving performance
A collection of Eclipse shortcuts that new graduates find useful
It's just now, but a collection of commands that frequently appear in Rails
A collection of simple questions for Java beginners
A collection of RSpecs that I used frequently
[Rails] Volume that displays favorites and a list of favorites
Basic knowledge of computer science and its practice that you should know for improving performance
Generate a unique collection of values from a collection that contains duplicate values
Explanation of Ruby on rails for beginners ③ ~ Creating a database ~
A breakthrough that you are not good at self-study and immediately rely on others (for yourself)
List of copy and paste collections that are useful when creating apps with Ruby on Rails
Gradle TIPS collection (for myself)