Sometimes a small change can have a huge effect…

Recently, a client switched from serving their product images through AWS S3 and Cloudfront to Cloudinary.

Although Cloudinary was delivering optimally sized and better compressed images, there was a noticeable delay before the images arrived, and some of this delay was due to the overhead of creating a connection to another origin (the delay existed with the S3 / Cloudfront combination too).

This excerpt from a WebPageTest waterfall illustrates the problem – Chrome starts to request the product image at 900ms into the page load, and there's a further wait of 600ms for DNS to be resolved, a TCP connection to be setup and TLS to be negotiated before the HTTP GET request for the image is actually sent.

(WebPageTest: Chrome, Cable, London)

I'm not quite sure why Chrome waits so long before initiating the request – it's a normal image element so easily discoverable by the preloader – but from memory, Chrome throttles requests for resources in the body until the those in the head have been completed so perhaps that's the case here.

Waiting 1.7s for the main product image to appear is far from ideal so how can make the image display sooner?

Preload?

The rel=preload Resource Hint is the first option that might spring to mind, but as Chrome prioritises preloads above all other content, and we didn't want to delay the stylesheets and scripts in the head it was discounted. (As an aside, Harry and I eventually decided to remove the font preloads on this site too).

Another challenge with rel=preload is that the exact resource has to be specified. For resources that are common across multiple pages (stylesheets, scripts etc) that's fairly easy to implement, but where the content changes by page, and may not be consistent e.g. search results, landing pages etc., it's a bit more involved.

As we didn't want the overhead of rel=preload we looked at how the rel=preconnect hint might help instead.

Preconnect?

rel=preconnect is often recommended for origins that have important resources but can't be discovered by the browser's preloader e.g. third-party tags that get injected via scripts, fonts from other origins etc., but as we'd already implemented preconnects for the six most important origins using the link element in the page I wasn't keen to add more.

(Generally I don't use more than six rel='preconnect dns-prefetch' declarations as Chrome has a limit on parallel DNS lookups – the limit was six, but might be slightly different now)

Most examples of Resource Hints show the markup based syntax but Resource Hints can also be specified via the link HTTP header (Edge actually only supports rel=preconnect as an HTTP header)

Implementing the hint as a header allows it to be applied across the whole site with a single configuration change, and as the hint is received in the headers the browser doesn't even need to start parsing the HTML to discover it.

So that's how we chose to implement preconnect:

link: <https://res.cloudinary.com>; rel=preconnect

The impact on the waterfall is pretty dramatic – Chrome starts establishing the connection as soon as it receives the initial chunk of the response for the page. This removes the connection overhead from the critical path for the image, and even though the request for the image still isn't made until 900ms has elapsed, it completes at 1.2s, a whole 500ms improvement!

(WebPageTest: Chrome, Cable, London)

The visual improvement is just as dramatic (and we've since improved the rendering of the product images by another 300ms)

Real-World Performance

WebPageTest is of course a stable test environment and it might not represent what happens in the complexity of the real-world where there are different browsers, fast and slow devices, and varying levels of network quality.

Fortunately this client uses SpeedCurve LUX, and we'd already implemented a User Timing mark to track when the first product image loaded. The real-world metrics showed a 400ms improvement at the median and greater than 1s improvement at the 95th percentile.

All-in-all a substantial improvement!

Closing Thoughts

There's no doubt that rel=preconnect makes a huge difference to how soon the product images are being displayed, and I suspect (though didn't test) similar gains could be made using its markup based variant too.

Looking at the complete waterfall (not included it here) it looks like there's scope for further gains if the browser had a better understanding of which images to prioritise and perhaps in the future when Priority Hints have wider support we'll experiment with an importance="high" hint.

Using rel=preconnect as a HTTP header offers some other interesting opportunities for improving performance – as it doesn't rely on markup being parsed preconnect can be triggered by requests for stylesheets, scripts and more.

Google Fonts sometimes (but not always) preconnects to fonts.gstatic.com by including link: <https://fonts.gstatic.com>; rel=preconnect; crossorigin in the stylesheet response.

Stylesheet from fonts.googleapis.com preconnecting to fonts.gstatic.com

As many third-parties tags connect to other domains there are opportunities for this technique to be more widely used (though reducing the number of domains by fronting them with a single CDN domain is probably the more preferable option)

If you've got important content coming from another domain I'd certainly recommend experimenting with preconnect to see what benefits you can gain.

References / Further Reading

CanIUse Link: rel=preconnect

Resource Hints

Priority Hints proposal

Limits on number of parallel DNS requests in Chrome - Yoav, Ilya, Addy

User Timing and Custom Metrics

How the Browser Pre-loader Makes Pages Load Faster

Preloading Fonts and the Puzzle of Priorities

Thanks

Finally I'd like to thank Charlie who encouraged me to share what we learned.

And if you'd like some help improving the performance of your site feel free to get in-touch