Optimizing for faster page loads

Performance is an important consideration in user experience. So what can we do to make pages load more quickly, perform more efficiently, and feel fast and responsive to users?

Before making optimizations, it's a good idea to analyze current load time, and identify any bottlenecks. In the Chrome Developer Tools, you can do this with the Timeline tab. It shows every step the browser takes to load a page, how long each step takes, which processes block other processes, and which ones happen concurrently. (For a more high-level analysis, tools like YSlow and PageSpeed Insights will test a page and suggest specific optimizations for you.)

If a page is loading slowly, it could be one request that's taking a long time, an inefficient sequence of requests, or that some processes are blocking resources unnecessarily.

There are three main areas we can potentially optimize:

Defining critical resources

A resource is considered ‘critical’ if it will block rendering of a page. By default, HTML, CSS and JavaScript are all blocking resources. That means a page won't load until the browser has received and parsed or executed all files of these types.

There are some cases where it's acceptable for a resource to block, like if the resource is needed for the page to display satisfactorily. Examples could be CSS for the page in question, or JavaScript necessary for a user to interact with the page (e.g. if the site is a single-page application using a JavaScript framework). But we don't always want this blocking behavior. Examples of resources where it isn't desirable would be stylesheets for other environments (like mobile, or print), or JavaScript for things like tracking or analytics that the user doesn't care about.

So the first set of optimizations we can make involves minimizing the number of critical resources a page has to request.

Start by making sure every (internal and external) library or framework is actually necessary, and that the performance cost of requesting it is worth the benefit it's providing to your users. Then you can move onto the other optimizations.

For CSS, you can minimize critical resources by splitting your code into different files based on purpose. So styles that only apply to very large screens could go in a separate file, styles that only apply to print go in another separate file, and so on. Then when importing with link tags, you specify a rule with the media attribute:

<!-- Render-blocking in all cases -->
<link href="style.css" rel="stylesheet">
<!-- Render-blocking only for screens at least 1920px wide -->
<link href="large-desktop.css" rel="stylesheet" media="(min-width: 1920px)">
<!-- Render-blocking only for print -->
<link href="print.css" rel="stylesheet" media="print">

This way, resources not needed for the environment will still be downloaded, but won't block page rendering.

For JavaScript, your code must again be split into separate files. To specify that a resource should be non-blocking, you add the async attribute:

// Blocking
<script src="app.js"></script>
// Non-blocking
<script src="tracking.js" async></script>

It's also a good idea to include scripts not responsible for the layout of your page just before the closing body tag. This is because when the browser hits a script tag, it pauses and executes the JavaScript before continuing, which can unnecessarily delay page load. This has the added benefit that you don't have to listen for the window.onload event in your JavaScript, because if the parser has reached the script tag, you know the previous page elements have loaded already. You could also defer non-essential scripts until after the page has loaded, to stop them impacting the initial render.

Preparing your resources

Now we're not wasting time being blocked by requests for unnecessary resources, but we can save even more time by making sure the resources we do need are as small as possible. Fewer bytes over the network means faster responses (or lower latency).

For HTML, CSS and JavaScript, you can reduce file sizes by minification. Minifying strips out all comments and white space, groups CSS selectors where possible, and renames JavaScript variables and functions to single letters, creating an unreadable mess for humans, but generally a much smaller file that a browser can parse just fine. Other file types like images, video and fonts should be optimized, and images should be served at their display size to avoid the browser having to do the image resizing.

There are a lot of minifiers and optimizers to choose from, and plugins to include the popular ones in build processes (with Gulp, Grunt, Webpack etc.) so you can automate these tasks.

You can reduce the size of minified text files (HTML, CSS and JavaScript) even further with gzip compression. Browsers are generally set up to support gzip already, so it has to be enabled in the server config.

Fetching your resources

So we've minimized the number of critical resources, and made them as small as possible, but HTTP requests are still an expensive part of loading a webpage. There are several things we can do to ensure we're making them efficiently.

First, we need to look at the order in which we're requesting resources. The most important requests should happen first, so things like essential CSS aren't being blocked by some less important process. This is why link elements are usually placed at the top of a page inside the head.

We should also try to make as few requests, or roundtrips to the server, as possible. We can do this by concatenating CSS files and JavaScript files into a single file for each before minifying. Be sure to only include the essential resources in this bundle though, otherwise you won't be able to take advantage of unblocking, deferring or asynchronously loading the non-essential resources.

Another way to reduce requests is to inline CSS and/or JavaScript. While this might be appropriate for static single pages or very small projects, and can be useful for, say, a small amount of critical CSS or code not shared with other pages, it's generally not a great solution. You save a request, but the downsides are potentially repeating code across multiple pages (which is a problem for maintenance), not being able to use CSS pre or postprocessors, and bloating your HTML page. An HTML page should ideally be smaller than 14KB, otherwise the browser is going to have to make multiple roundtrips to download it anyway. On that note, if you have a lot of content to show on a page, you can load your HTML incrementally with AJAX, using techniques like pagination or infinite scroll to keep initial page load time to a minimum.

Finally, we can cut requests on subsequent visits to a page by saving certain resources locally. This is known as HTTP caching. If a resource hasn't changed since the last visit, why request it again? Caching is controlled by the server response rather than the request, so has to be set server-side. In the response headers, you can specify things like whether to allow caching, and how long the resource should be cached for. Once it has been downloaded and cached, when the client needs it a second time, the browser will first check the cache for the resource. If it's there, and hasn't expired, that's one less network roundtrip the browser has to make. Resources that don't change very often (e.g. libraries) or code that is the same for all users are the best kind of things to cache, whereas code that's regularly updated is better cached for little or no time.

Even with these techniques, optimizing page load is still a juggling act. Is including that library worth it for the amount we're using it? Is inlining that CSS to save a request a fair trade-off for how much it increases the size of our HTML file? It takes careful analysis and decision making to find the best compromise for developer convenience and user experience.