Speed Up Rails By Starting on the Front
Doug Avery, Former Senior Developer
Is your Rails website performing poorly? Are clients and users complaining about 5-10 second pageloads, timeouts on mobile devices, or images that take forever to display? In some cases the solution lies on the backend — caching requests, speeding up DB queries, etc — but often, there are front-end optimizations you can look at first, that could have a huge effect on how fast a site feels.
Evaluating front-end performance
Check out www.webpagetest.org. For a quick glance at your site, look at the grades in the top right for immediate problems, and also check the two “Speed Index” measurements for first and repeat views. Since webpagetest saves all your tests, you can re-run after making some changes to easily see what's improved.
Avoiding premature optimization
Performance optimization takes time and resources, both of which are often in short supply. It’s hard to find the point where optimizations start to cost more than they save, and every site and team have their own unique challenges. I’ve ordered the notes in this post in order of payoff: if you want to start optimizing but don’t know where to begin, the first few ideas on this list will probably give you the biggest bang for you buck.
The biggest, easiest performance win you can implement is gzipping HTML/JSON/JS/CSS/font file responses. It can drastically reduce the size of transferred files, but it’s easy to forget and isn’t in most default configs.
Enabling keep-alive is another easy win that speeds up asset delivery by reusing the existing connection between client and server. Turning it on should be a no-brainer.
Optimize user-uploaded images
You generally want to convert all uploaded images to JPGs, strip metadata, and optimize them as much as you can without heavy quality loss. Without this step, an admin or user can easily tank the pagesize by uploading a 24-bit PNG or similarly uncompressed image. I recommend Dragonfly for on-the-fly image optimization — it lets front-end developers and designers fiddle with image details without needing to go back and update all your existing images on production after the change.
Caching and CDNs
Set all assets — JS, CSS, images, fonts — to expire in a year, using either far-future expires or max-age Cache-Control headers. There are a lot of simple instructions available for setting this on the asset pipeline and for CDN assets.
If you set far-future caching, you need unique, fingerprinted filenames. The asset pipeline’s precompile command will already handle this, but in most cases it won’t happen automatically for user-uploaded assets. For example, if a user edits their avatar image locally and re-uploads it to the server with the same name, and you’re serving it with far-future expires headers, you’ll have a problem. If you’re uploading files with something like Carrierwave or Paperclip, make sure you’re taking this step.
Migrating user-uploaded images and Rails assets to a CDN can improve download time by moving asset servers “closer” to the user. Both caching and fingerprinting rules still apply to CDNs!
SPDY is a networking protocol that can speed up page performance considerably, especially on pages with a lot of assets (example). It’s available in the majority of browsers and ready for production use. It requires that you set up SSL, which is admittedly a hassle, but it isn’t much work beyond that.
(Note that if you enable SSL+SPDY on your site, you might not want to use a CDN for asset hosting, as this can counteract the benefits.)
(Note: If you're using SPDY, it's recommended that you DON'T inline your assets — see SPDY best practices. Thanks Robert Fletcher!)
As a general rule, loading JS in the <head> is a bad idea — you want to move it to the bottom of the <body>. However, in some cases moving it isn’t possible. If a design relies on Modernizr to add classes to the body as early as possible to create a consistent visual experience, Modernizr needs to remain in the head. In this case, try inlining the JS in a script tag instead of creating an extra, blocking request. This technique trades a faster initial pageload for slower subsequent pageloads (since subsequent loads will no longer load the file from browser cache). In a lot of cases the other pageloads are just barely slower, and it’s a good tradeoff.
As a site grows, it becomes unreasonable to bundle all styles into a single application.css file. Users can end up viewing pages that load 100% of a site’s styles but only use 5-10%. In cases like this, you can break up page-specific styles into individual files and load them with a scheme that uses the controller/action to find CSS files.
Taking it further, there’s a good chance that if your view-specific CSS is small and users aren’t visiting many instances of the same view, making them request a second stylesheet isn’t necessary — just like with JS, you can inline them onto the page. Check out this gist for the setup I've used to inline CSS and JS assets in the past.
If you have pretty small images or single unique images on a view, it could be worthwhile to inline them as base64 strings instead of sending them as separate requests, using Ruby’s Base64 module. (Warning: in IE8, your base64-encoded images need to be 32kb or smaller, or they simply won’t display.)
jQuery and UJS
Rails ships with jQuery 1.9.x and jquery-ujs built into application.js, but jQuery is a pretty big library, and presents some optimization opportunities:
- Switch to jQuery 2.x — it’s smaller, but means dropping support for IE8. You could also use browser conditionals to load 1.9 for IE8 and below and 2.x for above, but you still want to combine jQuery into a single file with your other scripts, so you might end up with something like application-oldjquery.js and application.js.
- Cut UJS, or cut them both — if you’re not using any UJS features, you can remove it, which might make jQuery an unnecessary dependency.
At the time of this writing there’s no jQuery-less alternative for UJS, but it sounds like the team has considered the idea.
Google’s pagespeed module gives you a number of tools that can speed up front-end performance by making minor tweaks (trimming the domain out of local URLs, inserting DNS prefetch tags, collapsing whitespace) or major ones (lazy-loading images). I encourage you to install it and investigate some of the filters. Each filter has documented “Risks” that are worth reading before you implement — some of them, like remove quotes are low-risk filters that you could probably implement today with no downside.
These are a few tricks we've used here at Viget to speed up Rails front-ends over the years. If you have of your own ideas, or questions about these ones, let me know in the comments!