What is Bootsnap?

Bootsnap is one of those unassuming gems, that sits in your project’s Gemfile without calling much attention to itself.

# Gemfile

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

But what does it do? And how does it work?

Bootsnap’s goal is to make Rails bootup as fast as possible (or ‘boot-snappily’ if you will).

Say hello to Bootsnap

(Important technical detail: 'Under the hood' Bootsnap is actually a Boot.)

The gem was created by Shopify, and solves a particular problem they had.

Shopify’s problem

Shopify have a lot of Ruby code. They also use Rails largely as a monolith, so much of this code sits within a single application - over 2.8 million lines. With all this application code, Rails takes a while to load.

Say hello to Bootsnap

"Actually, maybe I don't scale after all."

If a unit tests uses Rails, the test runner needs to load the whole of Rails. So when a Shopify developer runs a single test, they need to load all that code. Before Bootsnap, this could take the Shopify monolith 25 seconds. This was a big problem that Shopify needed to solve1.

How Bootsnap speeds things up

Bootsnap caches the results of the last time that your application loaded up2. It does this in two ways:

  • Caching the $LOAD_PATH
  • Caching compiled Ruby code

Let’s look at each of these in turn.

Caching the LOAD_PATH

As you may remember from our previous article on loading files in Ruby, when Ruby comes across a require statement that it hasn’t seen before, it searches through the $LOAD_PATH to find that file. This may seem like a pretty speedy process. After all Ruby already has a list of places to look, how hard can it be?

Well, in some cases, there might be hundreds of entries in the $LOAD_PATH3. Ruby has to look through every one of these directories to find the matching file4.

Say hello to Bootsnap

"In one of these boxes you say?"

Bootsnap reduces this work by scanning the files that are in each directory when the application boots, and keeping a record of which files are stored where. When it comes time to require a file, Bootsnap can tell Ruby exactly where it needs to look.

Say hello to Bootsnap

Ruby ❤️ Bootsnap

Much faster!

Caching compiled Ruby code

The other thing Bootsnap caches is compiled Ruby code. This might seem a bit strange if you’re used to thinking of Ruby as an ‘interpreted’ language.

How are intepreted languages different from compiled languages?

With an interpreted language, the code is read by the computer shortly before it is executed.

With compiled languages, the computer reads through the whole program and converts it into a different format before attempting to execute any of it. The new format is easier for the computer to understand (typically machine code).

For example, to run ‘C’ code, you’ll first need to compile it. This step generates machine code in an executable file. Then, to execute the code, you then need to run this generated file in a separate step.

But Ruby is also compiled

However, this distinction between compiled and interpreted languages is fuzzier than you might think.

Unlike C, Ruby doesn’t compile all your code ahead of time, but there is a compilation step involved in running Ruby code. When Ruby first loads a file, the code goes through a build process.

The Ruby Build Process

Excerpt from Ruby Under the Microscope (which is interesting - you should read it)

This turns the Ruby file into a nice neat little package that the Ruby engine can then run. Specifically, it turns it into bytecode.

What is bytecode?

Bytecode is a lower-level language, that can be interpreted by the Ruby Virtual Machine5.

You can do this compilation step yourself to see what your Ruby code looks like when it has been turned into bytecode.

# This method...
def hello
  puts "hello, world"

# When compiled by Ruby...
puts RubyVM::InstructionSequence.disasm(method(:hello))

## Turns into this bytecode...

== disasm: <RubyVM::InstructionSequence:hello@/tmp/method.rb>============
0000 trace            8                                               (1)
0002 trace            1                                               (2)
0004 putself
0005 putstring        "hello, world"
0007 send             :puts, 1, nil, 8, <ic:0>
0013 trace            16                                              (3)
0015 leave                                                            (2)

This process is deterministic. So a file compiled by the same version of ruby will always end up as the same bytecode.

(This RailsConf talk by Maple Ong gives a great description of how the Ruby Virtual Machine works.)

The Ruby Build Process

Given that this is an expensive process, it seems a little silly that we repeat the same task again and again, every time Rails boots up. Bootsnap has a different idea, why don’t we just cache the bytecode?

Bootsnap caches the bytecode in the tmp folder of your Rails application. Then, when Rails requires the file again, rather than having to re-compile the code into bytecode, it can just read the pre-compiled bytecode from disc. If the code changes, the cache will be busted, and the file will be re-compiled.

So how much faster does this make everything?

These two techniques together making rebooting Rails a lot faster. According to Shopify, using Bootsnap sped up their boot times from 25 seconds to 5 seconds. A pretty huge win for developer experience.

This is more noticeable with larger apps, but there’s a reasonable chance that this little gem is saving you a second or so every time you want to run a test or load up the Rails console.

So the next time you reload Rails, spare a thought for the friendly boot.

  1. Worth bearing in mind though that this is really a developer experience problem. A production app is generally only rebooted occasionally, so boot time isn’t that much of an issue. 

  2. So, the first time you ever load your Rails application, Bootsnap won’t make it load any faster. 

  3. I counted over 100 on a brand new Rails app. 

  4. Of course with the Rails autoloader, all the files in your application code are already ‘autoloaded’, and so Ruby doesn’t need to check through the $LOAD_PATH for these. However, there are lots of other bits of Ruby code that Rails does need to require (for example, gems and code from the standard library). 

  5. Bytecode is not as low-level as machine code. Machine code is written in binary, and is platform specific. When you compile a ‘C’ program, you need to compile it for a specific computer architecture. Ruby’s Virtual Machine means that your bytecode is platform independent, and can run on any computer than runs Ruby. 

Get more Rails Explained

Subscribe to get all the latest posts by email.

    We won't send you spam. Unsubscribe at any time.