Recently, I was following RailsConf 2018 conference where one talk in particular caught my attention more than others. “Web Performance with Rails” by Stefan Wintermeyer. In his talk, He tried to give a brief idea of what Web performance is, why it is so important, quality metrics of a well-performing Web page, and how we can maintain the overall quality. I will focus on wording Stefan Wintermeyer’s talk with references, straightforward descriptions, and comments. I tried my best to infer the core tips based on slides shared by the author and notes from the conference. I would like to explain the detail in two parts:
- General Web performance tips
Tools to Measure Web Performance
There are many tools, measurements, and qualities you can use to assess how your page works in terms of Web performance. Two most popular ones according to Stefan currently are:
- Google PageSpeed Insights
Both tools are far from specific and allow you to assess the performance of every page using rather generic, broad indicators.
WebPageTest is a battle-tested and mature solution. Its author is a widely well-known figure in the Web performance world. It’s easy-to-use and the results, based on multiple popular performance indicators, are presented in an accessible way. It’s also open-source so one can deploy their own instance. Stefan recommends it as it was used many times for many different projects and it has never let people down.
PageSpeed Insights primarily uses Google’s own Web performance indicator called Speed Score (and a host of other indicators, but this is the main one). It analyzes the performance of web page in both desktop and mobile environments. For each indicator, it suggests a set of guidelines to improve performance of specific elements of your page. The important thing is that Google’s search engine takes results of these measurements into account. Better performance translates to a higher rank in Google’s search results.
Rails Ways of Improving Web Performance
View layer: Fragment caching This technique is about caching a part of the view based on, for example, an AR model or the association that used to render it. Rails will know when to remove the cache using the updated_at property of the model. Stefan demonstrated a nice example of nested fragment caching—single table rows and the whole table separately.
Database layer: Counter cache
Sometimes we often display a number of associated objects, we usually end up with using model.associated_objects.count which triggers the COUNT SQL query on the database. A small but forgettable improvement here is including the use of counter cache— storing associated objects’ count as the parent property. This way, when we can set up a given association in an AR model with the counter_cache option on, the .count method will use this property instead of running additional SQL queries. The counter property is updated under the hood by AR callbacks.
Although a dedicated counter_cache option exists in ActiveRecord, Stefan demoed his own implementation of it. I am not sure why. Maybe he prefers explicit solutions over implicit AR magic?
Ref: RoR guides: Associations Basics (counter_cache is an option for associations definition in AR models)
HTTP: etag and last_modified I don’t want to describe in detail how etag and last_modified HTTP headers work here but just to put it simply: we use these headers to manipulate HTTP response cache on the browser side.
One can leverage these features using Rails controller method called fresh_when. Basically, it explicitly binds etag and/or last_modified headers of the response to the state of your application.
For example, if the response for GET /products/:id can be cached on the browser side and the cache should be invalidated only if the updated_at property of a given product changes, fresh_when is the way to go.
Controller + server: Page caching The fastest page is delivered by Nginx without even contacting Rails
Example: 500.html, 404.html and similar pages are served quickly by a server because we don’t need to bother Rails to do so. These views are already generated.
Rails allows us to do the same with controller responses using the caches_page feature. In general, it can tell the controller which page to pre-render and save in the public directory and what are conditions of invalidating this cached page (using the expire_page and expire_cache methods). We can also gzip cached pages on save. Nginx will use these and serve the pages quickly instead of engaging with the Rails app.
This works nice for pages that change rarely (e.g. a public static landing page). But sometimes we have custom, dynamic pages, with current user data for instance, During the talk, Stefan said that it’s possible to cache these, too, but the process is much more complicated and requires much greater effort.
One option to consider in this case is using a header for the client to cache the endpoint and set a proper Vary header.
Another thing to consider is the disk space required for storing the cache if you handle a sizeable number of users and disk space is cheap these days, isn’t it?
Uploads and images: ActiveStorage
Stefan highly recommends using ActiveStorage to serve images properly. No matter the tool, Stefan wants us to achieve is to serve different image resolutions and formats for different browsers and devices to accomplish optimal fit. The ActiveStorage interface allows us to manipulate images pretty easily. To identify browser/device types, we need some other tools what weren’t expressly brought up by the author. This is just kind advice and a good practice to follow when Web performance is a high priority.
Non Rails ways of improving web performance
Use HTTP/2 Wintermeyer claims that simply switching from HTTP/1.1 to HTTP/2 provides an average Web performance boost of 20% right out of the gate. Well, it depends on a number of factors, some of which are not always obvious and can be as complex as our Web systems are. Moreover, current good practices may ultimately prove a hindrance to you in the new HTTP/2 world. HTTP/2 itself doesn’t require encryption by default, but browsers with HTTP/2 support require it to force best practice. It will get you a free boost after switch to HTTP/2, but results may vary.
Brotli over Gzip Brotli is a compression algorithm based on gzip, with some additions and improvements for better compression ratio thrown in. Current Web servers and browsers support it and adding it is fairly easy, yielding an average compression gain of 10 to 20%. This means almost free Web performance improvement. We can set up your application to support both compression algorithms, but opt to use brotli over gzip. In case of browsers not supporting brotli (see CanIuse reference below), we can give the browser the ability to fall back to gzip.
Web performance is hard and tricky. There are also other factors to count in. A big part of the list above pertains to caching, making it even harder. we can only learn to use it wisely.