---
title: Slicing up Rails Application.js for Faster Load Times
teaser: 'How to profile page load performance in Chrome and break out modular JavaScript
  assets.

  '
tags: performance,rails
author: Oli Peate
published_on: 2017-02-27
---

When using a fast device it's easy to forget that page load times are impacted
by not just network speed but processing power too. Browsing the thoughtbot
website on a MacBook Pro with a fast internet connection and warm cache feels
snappy. Backed by a CDN, the homepage usually loads in under a second for me.

On less powerful devices with slow internet connections the page load times have
plenty of room for improvement. To assess this we can use Chrome dev tools to
throttle the [network][network-throttling] and [CPU][cpu-throttling].

[network-throttling]: https://developers.google.com/web/tools/chrome-devtools/network-performance/network-conditions
[cpu-throttling]: https://plus.google.com/+AddyOsmani/posts/NRsAqshb17n

## Profiling with Chrome dev tools

Here's a good starting point for profiling page load times with Chrome:

- CPU throttling: "Low end device (5x slowdown)"
- Network throttling: "Good 3G"
- "Disable cache" selected

If you're profiling the performance against a local instance of your Rails
application it's worth noting the configuration should be as close to production
mode as feasible. That means disabling debug assets (so concatenation and
compression take place) plus eager loading & caching of code.

With these settings applied, in the Network panel it was evident that the
`application.js` file was taking a long time to load:

![Transfer time of application.js, showing that it takes the duration of nearly
the entire page
load.](https://images.thoughtbot.com/blog-vellum-image-uploads/PDR0pVVMTYCpktUmIqPo_application-js-transfer-time.png)

The `application.js` file had a 213KB transfer size, which represents the
compressed "over the wire" size. This file alone took over six seconds with the
simulated network conditions. By contrast it's possible to [solve a Rubik's cube
in under five seconds][rubiks-cube] . Any page loads over three seconds have a
55% increase in bounce rate according to our analytics.

[rubiks-cube]: http://www.guinnessworldrecords.com/news/2015/11/confirmed-teenager-lucas-etter-sets-new-fastest-time-to-solve-a-rubiks-cube-wor

It's not just the transfer time which is problematic. The uncompressed size of
the JavaScript file was 786KB. That's a sizeable quantity of JavaScript to
evaluate. Switching to the Timeline panel and refreshing the page revealed the
following:

![Evaluation time of application.js, occupying the main thread of the browser
for
468ms.](https://images.thoughtbot.com/blog-vellum-image-uploads/agSGia8CSGaH2R4KuJz2_application-js-evaluation-time.png)

With the CPU throttling active it takes almost 500ms to evaluate the script.
Inspecting the JavaScript manifest file revealed what's building up the payload:

```javascript
//= require_self
//= require modernizr-2.7.1-custom
//= require jquery
//= require jquery_ujs
//= require velocity
//= require velocity.ui
//= require d3
//= require moment
//= require moment-timezone
//= require iframeResizer.min
//= require_tree .
```

The largest contributions were from third-party libraries such as `d3.js` and
`moment.js`. Fortunately these libraries are only used on two pages located
deeper in the site, making them ideal candidates to be removed from the
main JavaScript file.

The extracted libraries can still be combined as a single page-specific script
so only one additional HTTP request is made on the relevant pages. The upside of
this change is significant — the `application.js` file is now 52KB when
compressed:

| application.js   | Before | After | Change |
|------------------|--------|-------|--------|
| transfer size    | 213KB  | 52KB  | -76%   |
| content size     | 786KB  | 159KB | -80%   |

In turn this reduced the transfer and evaluation times of the JavaScript file.
With the same throttling applied:

- Overall homepage load times decreased 7.5% (from 7.38s to 6.83s on average).
- `application.js` evaluation time decreased 66.5% (from 516ms to 173ms on
  average).

## How to add page-specific JavaScript in Rails

One of the most straightforward techniques I've found for introducing
page-specific JavaScript with the Rails asset pipeline is to use subdirectories.
Javascript assets can be organised like this:

![Rails JavaScript assets structure with page-specific content in subdirectories
containing an index.js
file.](https://images.thoughtbot.com/blog-vellum-image-uploads/GrUX2bahRCm26MkWUA11_assets-structure.svg)

### Leverage `require_directory` in manifests

The main `application.js` and `index.js` files can include any vendor scripts
declaratively and use the [`require_directory` directive][sprockets-require-directory]
from Sprockets to include the sibling scripts located in the same directory as
the manifest.

[sprockets-require-directory]: https://github.com/rails/sprockets/tree/d81ca88b959950954cbc1dd76c45a6fbe41b504f#the-require_directory-directive

```javascript
// Example admin/index.js:
//
//= require_self
//= require vendor_script_a
//= require vendor_script_b
//= require_directory .
```

The `require_directory` directive is useful if you want to avoid declaring each
of your required JavaScript files in the manifest.

### Assets precompile

Next step is to ensure your new JavaScript files are precompiled by the asset
pipeline. Editing `config/initializers/assets.rb`:

```ruby
Rails.application.config.assets.precompile += %w(
  admin/index.js
  blog/index.js
)
```

### Include a script tag for the new asset

On the page or layout where you'd like to include the new files add a script tag
(this example uses HAML):

```haml
= javascript_include_tag "admin" # This references admin/index.js
```

By naming each of our new manifests `index.js` the asset is referenced by the
directory name. In this example `admin/index.js` is referenced as `admin`. This
leverages the Sprockets [index files][sprockets-assets-index] feature.

[sprockets-assets-index]: http://guides.rubyonrails.org/asset_pipeline.html#using-index-files

The indirect referencing of index files by directory name could be harder to
reason about for unfamiliar contributors to your codebase. If you'd rather avoid
this convention (or 'magic') then an alternative is to use references with
paths. A file called `admin/admin.js` would be referenced as `admin/admin` (N.B.
the second step of asset precompilation needs to match this too).

### Test what matters

What's to stop a future developer from unwittingly undoing this performance
optimisation by adding a `require_tree` directive to the `application.js` file?

When performance matters then it must be treated as a first class citizen like
any functional changes to the code. That means we should test it!

Rather than a direct performance measurement, which would vary according to the
hardware the tests are run on, it's simpler in this scenario to check the
page-specific JavaScript isn't present elsewhere. This style of spec does fall
on the side of testing implementation detail rather than outcome, but it'll
catch regressions nonetheless.

Using Capybara with the [`capybara-webkit`][capybara-webkit] driver (other
JavaScript-enabled drivers work too) we can write a feature spec which checks
the libraries are no longer included globally:

[capybara-webkit]: https://github.com/thoughtbot/capybara-webkit

```ruby
describe "Assets", type: :feature, js: true do
  describe "application.js" do
    it "does not include page-specific content" do
      visit root_path

      expect("d3").to be_undefined_in_js
      expect("moment").to be_undefined_in_js
    end
  end

  private

  RSpec::Matchers.define :be_undefined_in_js do
    match do |actual|
      evaluate_script("typeof #{actual} === 'undefined'")
    end

    failure_message do |actual|
      "expected JS not to include #{actual}"
    end
  end
end
```

Now any regression will be caught. If the developer working on the code needs
further context they can find the commit which introduced the spec to trace why
the JavaScript is modularised.
