Andy Davies

Web Performance Consultant

Experimenting With Link Rel=preconnect Using Custom Script Injection in WebPageTest

The preconnect Resource Hint is a great way to speed up content that comes from third-party origins – it’s got relatively low overhead (though it’s not completely free) and is generally easy to implement.

Sometimes it produces small improvements, and sometimes more dramatic ones!

Browsers typically only make a connection to an origin just before they request a resource from it, so when resources are discovered late such as CSS background images, fonts, or script injected resources, or resources are considered a low priority, as Chrome does with images, the delay in making the connection becomes part of the critical path for that resource.

Preconnect enables us to create the connection in advance – removing it from the critical path for a resource, allowing the resource to be loaded sooner and hopefully improving the overall performance of a page too.

Implementing preconnect is often one of the first improvements I get clients to action but I’ve never been completely happy with my process…

Lighthouse offers some recommendations on which origins to preconnect to, but I tend to use a combination of WebPageTest and DevTools to identify candidates.

I make recommendations to the client I’m working with, their development team implement the preconnect directives and then we typically check the effect in a pre-production environment and adjust as necessary.

If I’m sat with the development team these cycles can be quick, but if the client’s using an external development team, or the team’s offshore they can be long.

What I wanted was a way to experiment, evaluate the options and demonstrate the gains before a client commits my recommendations to code.

Then I remembered WebPageTest has the ability to inject a custom script into the page being tested… I could create a script that adds preconnect directives and see what effect different options have on page speed.

Injecting the Script

At the bottom of the Advanced Tab there’s a text box labelled Inject Script, any script placed in here will be injected into the page shortly after it starts loading.

WebPageTest's Advanced Settings Panel showing the Inject Script text box

The script I use to create the <link rel=preconnect elements loops around an array of origins, adds a link element for each to a document fragment, and then adds the fragment to the DOM.

(function () {
   var entries = [
       {'href': 'https://res.cloudinary.com'}
   ];

   var fragment = document.createDocumentFragment();
   for(entry of entries) {
       var link = document.createElement('link');
       link.rel = 'preconnect';
       link.href = entry.href;
       if(entry.hasOwnProperty('crossOrigin')) {
               link.crossOrigin = entry.crossOrigin;
       }
       fragment.appendChild(link);
   }
   document.head.appendChild(fragment);
   performance.mark('wpt.injectEnd'); // Not essential 
})();

More origins can be added to the entries array as needed, and if an origin serves fonts the crossOrigin: 'anonymous' property should be added too.

For example, if both your images and fonts were hosted on Cloudinary, the first entry creates a connection for the images, and the second entry a connection for the fonts.

   var entries = [
       {'href': 'https://res.cloudinary.com'},
       {'href': 'https://res.cloudinary.com', 'crossOrigin': 'anonymous'}
   ];

I’ve wrapped the script in an IIFE to limit the scope of its variables, and avoid clashes with any existing ones in the page.

When Pat introduced script injection he described it as ‘a bit racey’ i.e. you can’t exactly be sure when it’s going to execute, so I include a performance mark to record when the script finishes execution.

Also when there’s no evidence of preconnects improving performance, the mark is a good sanity check that I actually remembered to include the custom script!

Putting it into Action!

So what difference can preconnect make?

I used the HTTP Archive to find a couple of sites that use Cloudinary for their images, and tested them unchanged, and then with the preconnect script injected.

Each test consisted of nine runs, using Chrome emulating a mobile device, and the Cable network profile.

There’s a noticeable visual improvement in the first site (https://www.digitaladventures.com/), with the main background image loading over half a second sooner (top) than on the unchanged site (bottom).

Filmstrip comparing the perfomance of Digital Adventures, with (top) and without (bottom) a preconnect to Cloudinary Top row site with preconnect, bottom row site without

Comparing the waterfalls, the gap between the end of the creating the network connection and the start of request for the image shows the preconnect has encouraged Chrome to connect sooner than it would have by default.

And by removing the network connection setup from the critical path Chrome can start to fetch the images sooner too.

Waterfall showing preconnect to Cloudinary With Preconnect to Cloudinary

Waterfall without preconnect to Cloudinary Without Preconnect to Cloudinary

You may also notice the connection setup is faster in the test with the preconnect, this is something I often see and suspect it’s due to network contention being lower at the start of the page load.

If you’d like to examine the results yourself, here’s a link to the comparison view, clicking on the labels to the left of the filmstrip will take you to the median run for each test:

https://www.webpagetest.org/video/compare.php?tests=190807_MR_41322698cc9e8666e96fe0a856204dbe,190807_AD_d1e711f0158521f9f872f86890508711

For the second site (https://hydeparkpicturehouse.co.uk/), there’s very little difference between the two tests – there's a small difference near the start of the filmstrip due to the server responding faster in the preconnect test (the menu bar at the bottom of the page appears sooner).

Filmstrip comparing the perfomance of Hyde Park Picturehouse Top row site with preconnect, bottom row site without

But the overall preconnect isn’t having any effect as once Chrome has discovered the image it’s immediately prioritising the request for it.

In the preconnect waterfall (top) you’ll see the Chrome starts creating the connection before the script to inject the preconnect has even finished executing – the purple vertical bar for the timing mark is after the DNS lookup for Cloudinary.

Waterfall showing preconnect to Cloudinary With Preconnect to Cloudinary

Waterfall without preconnect to Cloudinary Without Preconnect to Cloudinary

Again, here’s the comparison between the two tests if you want to explore further: https://www.webpagetest.org/video/compare.php?tests=190807_CP_c4385658104d23097b2084d15f7b3903,190807_0N_6d3eed6797303b7a4243e00335159125

There other options to improve the performance of both the sites tested and if these were implemented the preconnect may become more or less important – testing is the key thing!

Use Preconnect Selectively

Given the half a second improvement in the first test, you might be tempted to "preconnect all the things!", but I’d encourage you not to – well at least without testing thoroughly.

Browsers have limits on the number of concurrent DNS requests they can make, and creating a new HTTP connection may require certificates to be fetched, and these certificates will compete with other perhaps more critical resources for the network connection

Even without the TLS certificate overhead, adding extra preconnects may actually make things slower as it can change the order in which resources are retrieved, which can increase competition for the network or the browser’s main thread.

I re-ran the test for https://www.digitaladventures.com/ with a few more preconnects added:

   var entries = [
       {'href': 'https://res.cloudinary.com'},
       {'href': 'https://connect.facebook.net'},
       {'href': 'https://www.google-analytics.com'},
       {'href': 'https://cdnjs.cloudflare.com'},
       {'href': 'https://client.crisp.chat'},
       {'href': 'https://www.googleadservices.com'},
       {'href': 'https://www.gstatic.com'}
   ];

And the resulting test (top) was marginally slower than the test with a single preconnect (bottom) – in this case across multiple tests the background image was about 100ms slower rendering but I’ve seen worse examples.

Filmstrip showing effect of too many preconnects Top row site with many preconnects, bottom row site with only one

Again, here’s the comparison between the two tests if you want to explore further: https://www.webpagetest.org/video/compare.php?tests=190807_2B_7f2d2f22147f6207e64bf04ce7e06f1a,190807_MR_41322698cc9e8666e96fe0a856204dbe

We could refine the preconnect list to identify which one actually make the page faster, and which make it slower but I’ll leave that as an exercise if you want to have a play.

Closing Thoughts

In this post I’ve focused on two sites that use Cloudinary but I’d expect to see similar results with any site that hosts their images using a third-party such as Imigix, Kraken.io, Cloudfront etc.

And from what I’ve seen so far, I think many sites that host their images on third-party services would see improved performance if browsers automatically connected earlier but I’ve got to finish analysing the data from the 2,000 tests I ran over the weekend before I can put more detail on that.

The case for automatically pre-connecting for non-image resources is probably a little more fuzzy – render blocking resources tend to be requested at high priority so there’s no delay in creating the connection, and even for lower priority resources such as async scripts, downloading them sooner means the browser will try to execute them sooner which may or may not be a good thing.

Preconnect is a great feature but due to the complexities of how browsers prioritise resources, and our habit of building pages where resources are split across origins it’s not always easy to get right.

Using script injection in WebPageTest enables me to experiment, to explore the complexities of which origins to preconnect too and which ones to avoid, and evaluate the results before I get clients to start changing their code.

And that’s a good thing!

Further Reading

Resource Hints

Improving Perceived Performance With the Link Rel=preconnect HTTP Header

Issue 317774: Chrome should pre-connect for resources discovered by the preload scanner

Comments