Profiling Rails Boot Time
Is your Rails app starting to feel sluggish when running common start up
commands like server
, console
and rake
? Let’s take a look at debugging
rails applications that take a long time to boot.
Most of the pain is going to come from the Gemfile and anything in
config/initializers
. Large Gemfiles and long lists of initializers add to the
time it takes for Rails to boot because Rails has to load all of those files and
run any setup code. There are a few options to diagnose which Gems and
initializers are more costly than others.
Benchmarking Boot Time
Before we start trying to improve the boot time of our application, we need some
baselines to measure any improvements against. Let’s use the rake environment
task as the base for our benchmark - this just loads the Rails environment and
then exits. We can time it with:
We use bundle exec
to make sure any preloaders (spring, zeus) don’t get in the
way and use a preloaded environment.
As a sample, I’m using the popular open source project Discourse. For an app as big as Discourse, a 4 second boot time is very good.
Profiling
We’ll look into a few ways to profile what’s slow and what’s fast. Let’s have some
fun and monkey patch require
. Open up config/boot.rb
and paste in:
This wraps the call to super
- the actual require
method - in a call to
Benchmark#realtime
which returns a float of the total time the code
block took to execute. We add a conditional to print the time and file name if
the file takes over 100ms to load. To get the output sorted simply pipe to
sort
.
Another great option is the Bumbler Gem. It profiles your application and shows you how long each gem or initializer took to load. Again, this is for the Discourse app.
Remedies
Now that we know what’s slow, there are a few options for dealing with slowness.
Update Gems
The easiest way, believe it or not, is to simply update to the latest version of
a slow loading gem. I have run into several instances where I was running an
older version that had a dependency on an old version of a slow loading gem
(like nokogiri). In the latest stable version that dependency was removed, and
I was able to shave off over a second of boot time in several different
applications by doing a simple bundle update gem_name
.
require: false
By default, any Gem listed in the Gemfile
gets loaded when Rails starts. This
means boot time usually grows with Gemfile size. However, it is rarely the case
that you need a particular gem to be loaded and available everywhere.
If you only need classes / functions from a gem in a handful of files, it is much
more performant to add require: false
to the Gem declaration. Then, in any
file(s) you need access to that gem, add a require gem_name
to the top of the
file.
If you look at the Discourse
Gemfile you will
see that many of the Gem declarations tag on a require: false
. That is
what helps Discource to boot so quickly despite having 93 gem dependencies.
Rails inheritance in lib/
This is a potential issue I have seen in a few places where a class was
inheriting from a Rails core class such as ActiveRecord::Base
or
ApplicationController
and then being referenced in an initializer. It appeared
that it was requiring Rails all over again (around a 500ms hit). Moving it into
a Gem or into the app
folder resolved that issue.
Faster routes…
This is one that I unfortunately have no solution for. A large routes file can
have a serious impact on boot performance. In one app I worked on I’ve seen it
take over 3 seconds to load the routes. In the bumbler output it is the
:set_routes_reloader_hook
initializer.