We’re not doing a good job
Page-load times in the ten-second range are still common on modern mobile networks, and that’s a fraction of how long it takes in countries with older, more limited networks. Why so slow? It’s mostly our fault: our sites are too heavy, and they’re often assembled and delivered in ways that don’t take advantage of how browsers work. According to HTTP Archive, the average website weighs 1.7 megabytes. (It’s probably heftier now, so you may want to look it up.) To make matters worse, most of the sites surveyed on HTTP Archive aren’t even responsive, but focus on one specific use case: the classic desktop computer with a large screen.
Article Continues Below
That’s awful news for responsive (and, ahem, responsible) designers who aim to support many types of devices with a single codebase, rather than focusing on one type. Truth be told, much of the flak responsive design has taken relates to the ballooning file sizes of responsive sites in the wild, like Oakley’s admittedly gorgeous Airbrake MX site, which originally launched with a whopping 80-megabyte file size (though it was later heavily optimized to be much more responsible), or the media-rich Disney homepage, which serves a 5-megabyte responsive site to any device.
Why are some responsive sites so big? Attempting to support every browser and device with a single codebase certainly can have an additive effect on file size—if we don’t take measures to prevent it. Responsive design’s very nature involves delivering code that’s ready to respond to conditions that may or may not occur, and delivering code only when and where it’s needed poses some tricky obstacles given our current tool set.
Fear not!
Responsible responsive designs are achievable even for the most complex and content-heavy sites, but they don’t happen on their own. Delivering fast responsive sites requires a deliberate focus on our delivery systems, because how we serve and apply our assets has an enormous impact on perceived and actual page-loading performance. In fact, how we deliver code matters more than how much our code weighs.
Delivering responsibly is hard, so this chapter will take a deep, practical dive into optimizing responsive assets for eventual delivery over the network. First, though, we’ll tour the anatomy of the loading and enhancement process to see how client-side code is requested, loaded, and rendered, and where performance and usability bottlenecks tend to happen.
Ready? Let’s take a quick look at the page-loading process.
A walk down the critical path
Understanding how browsers request and load page assets goes a long way in helping us to make responsible decisions about how we deliver code and speed up load times for our users. If you were to record the events that take place from the moment a page is requested to the moment that page is usable, you would have what’s known in the web performance community as the critical path. It’s our job as web developers to shorten that path as much as we can.
A simplified anatomy of a request
To kick off our tour de HTTP, let’s start with the foundation of everything that happens on the web: the exchange of data between a browser and a web server. Between the time when our user hits go and their site begins to load, an initial request pings back and forth from their browser to a local Domain Name Service (which translates the URL into an IP address used to find the host), or DNS, to the host server (fig 3.1).
That’s the basic rundown for devices accessing the web over Wi-Fi (or an old-fashioned Ethernet cable). A device connected to a mobile network takes an extra step: the browser first sends the request to a local cell tower, which forwards the request to the DNS to start the browser-server loop. Even on a popular connection speed like 3G, that radio connection takes ages in computer terms. As a result, establishing a mobile connection to a remote server can lag behind Wi-Fi by two whole seconds or more (fig 3.2).
Two seconds may not seem like a long time, but consider that users can spot—and are bothered by—performance delays as short as 300 milliseconds. That crucial two-second delay means the mobile web is inherently slower than its Wi-Fi counterpart.
Thankfully, modern LTE and 4G connections alleviate this pain dramatically, and they’re slowly growing in popularity throughout the world. We can’t rely on a connection to be fast, though, so it’s best to assume it won’t be. In either case, once a connection to the server is established, the requests for files can flow without tower connection delays.
Requests, requests, requests!
Say our browser requests an HTML file. As the browser receives chunks of that HTML file’s text from the server, it parses them procedurally, looking for references to external assets that must also be requested, and converts the HTML into a tree structure of HTML elements known as a Document Object Model, or DOM. Once that DOM structure is built, JavaScript methods can traverse and manipulate the elements in the document programmatically and CSS can visually style the elements however we like.
The complexities of HTML parsing (and its variations across browsers) could fill a book. Lest it be ours, I will be brief: the important thing is getting a grasp on the fundamental order of operations when a browser parses and renders HTML.
- CSS, for example, works best when all styles relevant to the initial page layout are loaded and parsed before an HTML document is rendered visually on a screen.
- In contrast, JavaScript behavior is often able to be applied to page elements after they’re loaded and rendered.
But both JavaScript and CSS present bumps on the critical path, blocking our page from showing while they load and execute. Let’s dig into this order of operations a bit.
Rendering and blocking
The quickest-to-load HTML document is one without extra external files, but it’s also not one you’ll commonly find. A typical HTML document references a slew of outside assets like CSS, JavaScript, fonts, and images.
You can often spot CSS and JavaScript in the HTML document’s head
as link
and script
elements, respectively. By default, browsers wait to render a page’s content until these assets finish loading and parsing, a behavior known as blocking (fig 3.3). By contrast, images are a non-blocking asset, as the browser won’t wait for an image to load before rendering a page.
Despite its name, blocking rendering for CSS does help the user interface load consistently. If you load a page before its CSS is available, you’ll see an unstyled default page; when the CSS finishes loading and the browser applies it, the page content will reflow into the newly styled layout. This two-step process is called a flash of unstyled content, or FOUC, and it can be extremely jarring to users. So blocking page rendering until the CSS is ready is certainly desirable as long as the CSS loads in a short period of time—which isn’t always an easy goal to meet.
Blocking’s value with regard to JavaScript almost always undermines the user experience and is more a response to a lingering JavaScript method called document.write
, used to inject HTML directly into the page at whatever location the browser happens to be parsing. It’s usually considered bad practice to use document.write
now that better, more decoupled methods are available in JS, but document.write
is still in use, particularly by scripts that embed advertisements. The biggest problem with document.write
is that if it runs after a page finishes loading, it overwrites the entire document with the content it outputs. More like document.wrong
, am I right? (I’m so sorry.) Unfortunately, a browser has no way of knowing whether a script it’s requesting contains a call to document.write
, so the browser tends to play it safe and assume that it does. While blocking prevents a potential screen wipe, it also forces users to wait for scripts before they can access the page, even if the scripts wouldn’t have caused problems. Avoiding use of document.write
is one important step we can take to address this issue in JavaScript.
In the next chapter, we’ll cover ways to load scripts that avoid this default blocking behavior and improve perceived performance as a result.