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?
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.
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.
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)
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!
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
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
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