Speeding up the web with the Save-Data header

One of the advantages of running a small blog that is hardly read by anyone is the ability to experiment with new technology and not have to worry about the potential impact on avid readers (hah!). So this weekend I decided to have a play with the not so new Save-Data header (initial specification August 2015).

What is it?

The Save-Data header field is a request header that indicates a client’s preference for reduced data usage. Essentially it is a setting that a user can enable that allows less data to be transferred over the connection (assuming a website is setup to adapt to it). A request from a client looks like this:

GET /image.jpg HTTP/1.0
Host: example.com
Save-Data: on

Note the Save-Data header. A lot of what can happen at this point is up to the server. It sees the preference and can adapt accordingly.

According to Brendan Abbott, a developer at Shopify: “20% of Indian/Brazilian requests contain this header”. Those are two huge markets. That’s a lot of people requesting to use less data while browsing the web!

Adapting using JavaScript

Now it is possible for the server to serve completely different markup depending on if the header is on or off (more on this later). But what about if you don’t have that much control over the server setup? This is the situation I am in at the moment as I’m using GitHub Pages for hosting. Thankfully there is a way to detect this user preference using JavaScript (thanks go to Tim Kadlec for pointing me in the right direction with this!)

if ("connection" in navigator) {
    if (navigator.connection.saveData === true) {
        document.documentElement.classList.add('save-data');
    }
}

Add the above snippet of code in your <head> and when a browser has “Data Saving Mode” enabled, the save-data class will be appended to the <html> element. If not enabled it is simply ignored.

So this is exactly what I have done with this website. If you enable “Lite mode” in Google Chrome or “Turbo” in Opera, less data will be sent when viewing the site.

Testing

Before I tell you what I changed lets have a look at the difference it has made. All tests were conducted using Web Page Test as it comes with a setting to include the header (Advanced settings –> Chromium –> Enable Data Reduction). The tests were conducted on a 3G Fast connection using a real Moto G4 phone. This is a well known phone in the web performance community. It’s now considered “legacy” (in the First World), but it still has a very large user base across the globe. So it is a great device to use as a performance baseline.

Results

Homepage

Key metrics pulled out from the WPT results for the homepage:

Metric Without Save-Data With Save-Data diff (%)
Visually Complete (ms) 2,007 1,607 -20
Fully Loaded (ms) 3,900 2,940 -25
Number of Requests 25 10 -60
Bytes transferred 240,008 149,095 -38

You can view both sets of results and the comparison in the links below:

Blog post

Key metrics pulled out from the WPT results for a blog post:

Metric Without Save-Data With Save-Data diff (%)
Visually Complete (ms) 3,398 1,640 -52
Fully Loaded (ms) 5,263 3,748 -29
Number of Requests 30 15 -50
Bytes transferred 490,917 399,936 -19

You can view both sets of results and the comparison in the links below:

Observations

As you can probably see there are some huge percentage differences in the results. The time for visually complete and fully loaded dropped in the range of 20 to 52%! The number of bytes transferred dropped for the homepage and blog post by 38% and 19% respectively.

One of the more impressive observations (for me) is when you switch the Web Page Test comparison filmstrip view to use an interval of 60fps, the Save-Data enabled view renders completely in a single frame (or ~16.6ms). That’s compared to a large number of frames when disabled (see image below):

Save-Data enabled rendering a page in 1 frame (16.6ms)
Rendering the page view in 1 frame (~16.6ms)

So what was changed?

By detecting the Sava-Data preference using JavaScript and appending a save-data class to the <html> tag, we now have a few options available to optimise the website using only CSS changes.

No more background images

All background images were removed (or they were replaced with a solid colour). It’s a simple task:

.example-component {
	background: url('images/example-background.png') top left no-repeat;
}

.save-data .example-component {
	background: none;
}

By setting the background to none with a more specific selector, the image won’t be downloaded by the browser (since it is no longer needed). I had a number of background images all over the site so it saved a fair few bytes and requests.

Remove the web font

A large asset in many sites is the web font(s) used. This is especially true if using older versions like WOFF or EOT. Thankfully you can disable a font download using CSS alone:

.example-component {
	font-family: 'Dosis', Geneva, Tahoma, Verdana, sans-serif;
}

.save-data .example-component {
	font-family: Geneva, Tahoma, Verdana, sans-serif;
}

By removing the reference to the web font, the browser will no longer download it (even though it is still defined using @font-face). The key to getting this to work is to make sure you don’t have a single reference to the font anywhere on the page. As soon as it is referenced by a font-family property, it will be downloaded!

Disable the Service Worker

I have a service worker running on this site that is automatically generated using Workbox.js. The way I have the service worker setup is to precache a whole bunch of assets when a user first hits the page (font & images mainly).

One of the major downsides in doing this is a user will potentially be doubling up on the number of requests and bytes downloaded. The reason for this is because the browsers HTTP cache and the service worker cache are totally different, and they don’t share the assets downloaded.

Service worker precache doubling requests for assets
Precache doubling the requests and bytes to specified assets.

The first set of downloads primes the standard browser (HTTP) cache. Once the service worker registers and activates the service worker cache is primed (hence duplicating requests).

So by wrapping the service worker registration in a similar JavaScript check we used when adding the CSS classname, we can stop the service worker from registering:

if ('serviceWorker' in navigator) {
    /* by default we want to load the SW */
    var loadTheSW = true;

    /* check for connection support and if they want to save data */
    if ('connection' in navigator && navigator.connection.saveData === true) {
        /* they support connection, and want to save data, don't load the SW */
        loadTheSW = false;
    }

    /* decide what to do with the SW */
    if(loadTheSW){
        window.addEventListener('load', () => {
            navigator.serviceWorker.register('/sw.js').then(registration => {
                console.log('SW registered: ', registration);
            }).catch(registrationError => {
                console.log('SW registration failed: ', registrationError);
            });
        });
    }
}

The registration is stopped therefore the activation and preloading of assets. This also has the added advantage of stopping the Workbox production JavaScript from downloading too (since it is listed as an importScripts() in the sw.js file).

Remove tracking (and any additional JavaScript)

This isn’t something I have done yet, as I’ve just added a custom dimention in Google Analytics to check the percentage of users coming to the website with the Save-Data header present. But in a couple of months time I will wrap the GA code in the same check as the service worker above. Users who want to save data will no longer be required to download any tracking code (plus the additional DNS / TCP / TLS overhead involved).

This can easily be extended to any JavaScript on a page. Assuming you have a good no-JavaScript fallback for a component (you are using Progressive Enhancement after all aren’t you?), you could just not load the JavaScript it requires.

Serve smaller images

Another tactic that I haven’t adopted due to the limitations of GitHub pages, but you could quite easily serve a different set of smaller images to users who want to save data.

One way to do this would be to serve a totally different HTML file for users wanting less data. For example using an Apache .htaccess file you could do this (Thanks go to Barry Pollard for this code):

RewriteCond "%{HTTP:save-data}" "on" [NC]
RewriteCond /www/htdocs/%{REQUEST_URI}/index.html.small -f
RewriteRule ^(.*)/$ "$1/index.html.small" [END]

Within the index.html.small you could have references to a completely different set of (smaller) images. There are other ways of doing this too, so I recommended reading Delivering Fast and Light Applications with Save-Data for alternative methods.

Browser support

So this question comes up for every “new” feature. What does the browser support look like? Well according to MDN, at the time of writing it is mainly Chrome and Opera (on Desktop and Android) that support the Save-Data header. Samsung Internet is also listed as a “Yes”, but I couldn’t see a way of enabling it in the options when I looked on my phone (version 9.4). I’d love to know if this option is available or coming soon to SI.

Conclusion

So with a little bit of JavaScript, backend code and some thought, it is possible to deliver your website using less data. This is extremely important for the next billion users i.e. users on older devices and expensive data plans who want to access your content but simply can’t due to the ever expanding bloat on the web today. So why not give a user the option to use less data for your website? After all, less data doesn’t mean a lesser experience!