The Device Memory API
The range of capabilities of devices that can connect to the web is wider today than it's ever been before. The same web application that's served to a high-end desktop computer may also be served to a low-powered phone, watch, or tablet, and it can be incredibly challenging to create compelling experiences that work seamlessly on any device.
The Device Memory API is a new web
platform feature aimed at helping web developers deal with the modern device
landscape. It adds a read-only property to the navigator
object,
navigator.deviceMemory
, which returns how much RAM the device has in
gigabytes, rounded down to the nearest power of two. The API also features a
Client Hints Header, Device-Memory
, that reports
the same value.
The Device Memory API gives developers the ability to do two primary things:
- Make runtime decisions about what resources to serve based on the returned device memory value (e.g. serve a "lite" version of an app to users on low-memory devices).
- Report this value to an analytics service so you can better understand how device memory correlates with user behavior, conversions, or other metrics important to your business.
Tailoring content dynamically based on device memory
If you're running your own web server and are able to modify the logic that
serves resources, you can conditionally respond to requests that contain the
Device-Memory
Client Hints Header.
GET /main.js HTTP/1.1
Host: www.example.com
Device-Memory: 0.5
Accept: */*
With this technique you can create one or more versions of your application
script(s) and respond to requests from the client conditionally based on the
value set in the Device-Memory
header. These versions don't need to contain
completely different code (as that's harder to maintain). Most of the time the
"lite" version will just exclude features that may be expensive and not critical
to the user experience.
Using the Client Hints Header
To enable the Device-Memory
header, either add the Client Hints <meta>
tag
to the <head>
of your document:
<meta http-equiv="Accept-CH" content="Device-Memory">
Or include "Device-Memory" in your server's Accept-CH
response headers:
HTTP/1.1 200 OK
Date: Thu Dec 07 2017 11:44:31 GMT
Content-Type: text/html
Accept-CH: <strong>Device-Memory</strong>, Downlink, Viewport-Width
This tells the browser to send the Device-Memory
header with all sub-resource
requests for the current page.
In other words, once you've implemented one of the options above for your
website, if a user visits on a device with 0.5 GB of RAM, all image, CSS, and
JavaScript requests from this page will include the Device-Memory
header with
the value set to "0.5", and your server can respond to such requests however you
see fit.
For example, the following Express route serves a
"lite" version of a script if the Device-Memory
header is set and its value is
less than 1, or it serves a "full" version if the browser doesn't support the
Device-Memory
header or the value is 1 or greater:
app.get('/static/js/:scriptId', (req, res) => {
// Low-memory devices should load the "lite" version of the component.
// The logic below will set `scriptVersion` to "lite" if (and only if)
// `Device-Memory` isn't undefined and returns a number less than 2.
const scriptVersion = req.get('Device-Memory') < 1 ? 'lite' : 'full';
// Respond with the file based on `scriptVersion` above.
res.sendFile(`./path/to/${req.params.scriptId}.${scriptVersion}.js`);
});
Using the JavaScript API
In some cases (like with a static file server or a CDN) you won't be able to conditionally respond to requests based on an HTTP header. In these cases you can use the JavaScript API to make conditional requests in your JavaScript code.
The following logic is similar to the Express route above, except it dynamically determines the script URL in the client-side logic:
// Low-memory devices should load the "lite" version of the component.
// The logic below will set `componentVersion` to "lite" if (and only if)
// deviceMemory isn't undefined and returns a number less than 1.
const componentVersion = navigator.deviceMemory < 1 ? 'lite' : 'full';
const component = await import(`path/to/component.${componentVersion}.js`);
component.init();
While conditionally serving different versions of the same component based on device capabilities is a good strategy, sometimes it can be even better to not serve a component at all.
In many cases, components are purely enhancements. They add some nice touches to the experience, but they aren't required for the app's core functionality. In these cases, it may be wise to not load such components in the first place. If a component intended to improve the user experience makes the app sluggish or unresponsive, it's not achieving its goal.
With any decision you make that affects the user experience, it's critical you measure its impact. It's also critical that you have a clear picture of how your app performs today.
Understanding how device memory correlates with user behavior for the current version of your app will better inform what action needs to be taken, and it'll give you a baseline against which you can measure the success of future changes.
Tracking device memory with analytics
The Device Memory API is new, and most analytics providers are not tracking it by default. Fortunately, most analytics providers give you a way to track custom data (for example, Google Analytics has a feature called Custom Dimensions), that you can use to track device memory for you users' devices.
Using a custom device memory dimension
Using custom dimensions in Google Analytics is a two-step process.
- Set up the custom dimension in Google Analytics
- Update your tracking code to
set
the device memory value for the custom dimension you just created.
When creating the custom dimension, give it the name "Device Memory" and choose a scope of "session" since the value will not change during the course of a user's browsing session:
Next update your tracking code. Here's an example of what it might look like. Note that for browsers that don't support the Device Memory API, the dimension value will be "(not set)".
// Create the tracker from your tracking ID.
// Replace "UA-XXXXX-Y" with your Google Analytics tracking ID.
ga('create', 'UA-XXXXX-Y', 'auto');
// Set the device memory value as a custom dimension on the tracker.
// This will ensure it gets sent with all future data to Google Analytics.
// Note: replace "XX" with the index of the custom dimension you created
// in the Google Analytics admin.
ga('set', 'dimensionXX', navigator.deviceMemory || '(not set)');
// Do any other other custom setup you want...
// Send the initial pageview.
ga('send', 'pageview');
Reporting on Device Memory data
Once the device memory dimension is
set
on the tracker object, all data you send to Google
Analytics will include this value. This will allow you to break down any metric
you want (e.g. page load
times, goal completion
rate, etc.) by device
memory to see if there are any correlations.
Since device memory is a custom dimension rather than a built-in dimension, you won't see it in any of the standard reports. To access this data you'll have to create a custom report. For example, the configuration for a custom report that compares load times by device memory might look like this:
And the report it generates might looks like this:
Once you're collecting device memory data and have a baseline for how users are experiencing your application across all ranges of the memory spectrum, you can experiment with serving different resources to different users (using the techniques described in the section above). Afterwards you'll be able to look at the results and see if they've improved.
Wrapping up
This post outlines how to use the Device Memory API to tailor your application to the capabilities of your users' devices, and it shows how to measure how these users experience your app.
While this post focuses on the Device Memory API, most of the techniques described here could be applied to any API that reports device capabilities or network conditions.
As the device landscape continues to widen, it's more important than ever that web developers consider the entire spectrum of users when making decisions that affect their experience.