What are HTTP Client Hints and how are they used?

Neil Cooper
February 1, 2023
EcommerceGuidesEngineering

What are Client Hints?

In short, client hints are an optional set of HTTP request header fields that a server can proactively request from the client, allowing information to be collected about the device, user, network and user-agent-specific preferences. For further reading, we recommend this article: https://developer.chrome.com/blog/automating-resource-selection-with-client-hints/.

How does it work?

To enable the server to accept hints, you must first insert the following header into your server’s config file, as shown in the example below:

1Accept-CH: DPR, Width, Viewport-Width, RTT, ECT, Downlink, Device-Memory

In a simple node app, this looks like:

1app.use((req,res,next) => {  
2res.append("Accept-CH","DPR, Width, Viewport-Width, RTT, ECT, Downlink, Device-Memory");
3})

Once this header is inserted, it allows the client to return responses containing the requested information. However, some information – such as width – isn’t sent cross-domain. In the example below, Amplience Dynamic Imaging service is being used to serve images, preventing the retrieval of the width header directly. For the demo below, this was overcome by routing the image requests via a local route in a node app:

1app.get('/retrieveImage/*', function(req,res,next){  
2let queryString = 
3querystring.unescape(querystring.stringify(req.query)).replace("$=","$");  
4var image = "http://cdn.media.amplience.net/i/bccdemo/" + req.params[0] + "?" + queryString + "&w=" + req.headers.width;      
5req.pipe(request(image)).pipe(res)
6})

This short snippet allows the app to collect the request headers and use them in the image request to Amplience, then retrieves the resulting image. In theory, this could also be used for other headers, changing the output to suit those headers.

Why should you use Client Hints?

At the moment, client hints aren’t supported by Safari – but don’t let this put you off using them! According to caniuse.com, the remaining browsers that do support client hints account for around 75% of all usage.

Using client hints gives us the following useful information:

Width

For an image request, this is the space available for the image, telling us the best image size to return based on the hints given by the browsers’ engine. The sizes attribute from an image is used to calculate this: e.g.

1sizes="(max-width: 576px) 100vw, 50vw"

You might recognize this from previous blog posts but if not - this attribute tells the browser to render the image at 100% of the viewport width when the viewport is 576px or less, otherwise render the image at 50% viewport width.

Usually, you’d make use of this information in a picture tag and create multiple srcs in a srcset attribute for each breakpoint to request an image that closely matches the render size:

1<picture>    
2<source srcset="        
3//cdn.media.amplience.net/i/bccdemo/pexels-pixabay-248304?$poi$&w=1024&sm=aspect&aspect=16:9 1024w,        
4//cdn.media.amplience.net/i/bccdemo/pexels-pixabay-248304?$poi$&w=760&sm=aspect&aspect=1:1 760w,         
5//cdn.media.amplience.net/i/bccdemo/pexels-pixabay-248304?$poi$&w=288&sm=aspect&aspect=1:1 577w,         
6//cdn.media.amplience.net/i/bccdemo/pexels-pixabay-248304?$poi$&w=320&sm=aspect&aspect=9:16 320w" media="(min-width: 320px)"         
7sizes="(min-width:1024px) 1024px,100vw,(min-width:760px) 760px, 100vw,(min-width:577px) 577px, 50vw,(min-width:320px) 320px, 100vw">   
8 <img class="d-block img-fluid" 
9src="//cdn.media.amplience.net/i/bccdemo/pexels-pixabay-248304?$poi$&w=1024&sm=aspect&aspect=1:1" width="100%" border=0 alt="" title=""/>
10</picture>

However, with client hints, we’re able to find out the exact width available. This means we no longer have to stretch or squash an image and can just use the example below for each of our image requests:

1<img class="img-fluid" src="/retrieveImage/pexels-pixabay-248304?$poi$&sm=aspect&aspect=1:1" sizes="(max-width: 576px) 100vw, 50vw" border=0 />

The picture tag solution to this would be to have lots of breakpoints to reduce the amount of stretching.

Stretching and squashing images leads to poor quality output, so using client hints may enable you to improve the quality of the requested image and save further bandwidth.

DPR

DPR is short for Device Pixel Ratio. This varies from device to device. The original retina displays from Apple were 2x, but now you can get displays up to 8x. You can use this value to change which image you request, but the browsers already factor this value in when they return the width value, multiplying the width value to account for DPR.

Whereas width and DPR are about the real estate for a request, RTT, ECT and Downlink are about the network environment. Save-Data is a slight anomaly, but allows the end user to choose to indicate that they wish to save data transferred.

•          RTT: Estimated effective round-trip time of the current connection

•          ECT: Effective type of the connection, meaning one of ‘slow-2g’, ‘2g’, ‘3g’, or ‘4g’

•          Downlink: Effective bandwidth estimate in megabits per second

•          Save-Data: Indicates whether extra measures should be taken to reduce the payload.

How can these be used?

If we go back to our earlier snippet:

1app.get('/retrieveImage/*', function(req,res,next){  
2let queryString = 
3querystring.unescape(querystring.stringify(req.query)).replace("$=","$");  
4var image = "http://cdn.media.amplience.net/i/bccdemo/" + req.params[0] + "?" + queryString + "&w=" + req.headers.width;      
5req.pipe(request(image)).pipe(res)
6})

We can alter this to add further logic to account for the network conditions. Let’s take Downlink and Save-Data as examples.

Firstly, we need to decide on the experience we’re giving end users. For example, to create a performant experience for users and Save-Data is turned on, we should reduce the number of bytes delivered. We can also assume that a smaller Downlink number should correspond to a smaller number of bytes delivered too, which is achieved by reducing the quality and/or size of the image returned.

For reference: 1 megabit/s = 128 kilobyte/s 2g signal is equivalent to 0.1 megabits/s 3g = 3 megabits/s 4g = 15 megabits/s

2g isn’t very common anymore, but 3g is experienced often enough to warrant altering your web app to account for reduced performance. If you’re a retailer with adverts at a concert, improving the performance of your web app could make a huge difference as masts get overloaded by users in a small geographical area.

So, we could write in a simple ternary to request lower quality images dependent on the Downlink value, like this:

1let smartQlt =  ( req.headers.downlink < 1 ? 40 :                     
2req.headers.downlink < 3 ? 50 :                    
3req.headers.downlink < 5 ? 60 :                    
4req.headers.downlink < 8 ? 70 :                    
5req.headers.downlink < 10 ? 80 : 90 )

and something to reduce image sizes when Save-Data is turned on. Here, we have decided that reducing the quality to 50% and preventing 2x or 3x images from being sent is and adequate reduction for our users:

1let smartWidth = (req.headers['save-data'] ?(req.headers.width/req.headers.dpr) : req.headers.width)

We’ll add these into our image route and use the values to request the appropriate image from Amplience:

1app.get('/retrieveImage/*', function(req,res,next){  
2let queryString = 
3querystring.unescape(querystring.stringify(req.query)).replace("$=","$");  
4let smartQlt =  ( req.headers.downlink < 1 ? 40 :                     
5req.headers.downlink < 3 ? 50 :                    
6req.headers.downlink < 5 ? 60 :                    
7req.headers.downlink < 8 ? 70 :                    
8req.headers.downlink < 10 ? 80 : 90 )  
9let smartWidth = (req.headers['save-data'] ? 
10(req.headers.width/req.headers.dpr) : req.headers.width)  
11var image = "http://cdn.media.amplience.net/i/bccdemo/" + req.params[0] + "?" + queryString + "&w=" + smartWidth + "&qlt="+ smartQlt;      
12req.pipe(request(image)).pipe(res)
13})

Smart Images testing

We can give these a rudimentary test using Chrome Developer tools and an extension that enables the Save-Data header:

We’ve used a device with a high DPR (3.5) to make the difference a little more obvious.

In the network panel on the bottom of this image, you can see 3 pairs of images:

  • Images loaded with no throttling or Save-Data (901k and 311k)

  • Images loaded with fast 3g throttling (349k and 96.7k)

  • Images loaded with fast 3g throttling and Save-Data on (38.3k and 14.2k)

We’ve successfully modified the images we’re requesting based on the information the client passes us!

We hope this was a useful post for everybody. Keep your eyes peeled for more articles by the Amplience team.