MVP Code-Splitting in Rails with Webpacker

Webpack can do impressive minimization based on static analysis. But it's extremely easy to get the low-hanging fruit version of this in your Rails app with nothing fancier than Webpacker.

Here's an example. My company had a simple shopping cart and a web site full of React components. Black Friday was approaching, and we knew that fast sites make more money. Amazon's proven it in internal studies, and Google's done the same. So we wanted our shopping cart to load as quickly as it could. I and two other front-end devs dug into the problem.

We loaded up the shopping cart in WebPageTest and saw that the actual download time was very fast, but the time to first paint was not so good. To quote a Google guide on the topic:

consider an app that optimizes for a fast initial render, delivering content to the user right away. If that app then loads a large JavaScript bundle that takes several seconds to parse and execute, the content on the page will not be interactive until after that JavaScript runs. If a user can see a link on the page but can't click on it, or if they can see a text box but can't type in it, they probably won't care how fast the page rendered.

In other words, we were transmitting the data pretty quickly, but there was a delay between when the browser got the data and when the user was able to do or see anything. My theory was that we had this delay for a very simple reason: we were sending every React component used in the entire application down the wire. But this was an ecommerce business; you didn't get access to the application until you'd bought something, and in the overwhelming majority of cases, once you had bought something, you'd have no need to visit the shopping cart again. (There may have been issues in that business model, but that's a topic for another blog post.)

React has a reputation for being quite fast, but we were giving the browser hundreds of components to instantiate without using once. Worse, our code was set up in such a way that the first thing the JavaScript did was require all of the components in the application. So, not only were we instantiating tons of components we wouldn't use, we were doing all of that before we ever showed the user a single line of text.

How It Worked

  1. app/views/layouts/application.html.slim required index.js
  2. Webpacker generated index.js based on client/packs/index.js
  3. index.js imported all of our JS libraries
  4. index.js imported all of our components
  5. individual Rails views instantiated whichever React components they needed

I want to just take a second to say that this company had an absolutely fantastic engineering team, and in particular, some of the best front-end devs I've ever had the pleasure to work with. But we didn't have entirely clear focus or direction from the product team, and as a consequence, front-end performance had been entirely neglected as a dev priority up until that point — despite the fact that everyone on the web has known for decades that speed makes money. Likewise, from a code organization point of view, what we were doing made perfect sense; you need to require files before you use them. The problem was that we didn't very often stop to consider the performance tradeoffs.

Anyway, this problem was very easy to fix. We just needed a slightly different workflow.

The New Workflow

  1. app/views/layouts/cart.html.slim required cart-index.js
  2. Webpacker generated cart-index.js based on client/packs/cart-index.js
  3. cart-index.js imported only the libraries needed for the cart
  4. cart-index.js imported only the components needed for the cart
  5. individual Rails views instantiated whichever React components they needed

Is there a better way to solve this problem? Of course. We left the rest of the site slow, because we were focused on a Black Friday deadline and no other page's performance would have a similar impact on our bottom line. But did this solution get us a colossal performance improvement in exchange for about half an hour's effort? Also of course. And did we see increased sales? We did. We weren't running the kind of speed tests that Amazon and Google have famously run, so I can't claim we know for a fact that this speed change had any effect on our sales — but it probably did.

We were using client/packs/ as our Webpacker source dir, but that's very easy to change in webpacker.yml, and you can get the exact same results if you're using the Rails default location of app/javascript/packs. There's a lot more you can do to increase front-end performance in a Rails app, but this was a big win which took very little effort.

Caveat: you should only use a strategy this coarse for the one or two pages in your app that are most obviously high-priority. If you applied this strategy to too many pages, the maintenance would be a hassle.