DNS Resolution Adds Up

DNS resolution is cheap, but it ain't free. In scenarios when it counts, the `dns-prefetch` resource hint could give you a nice, slight edge in the front-end performance game.

I like the satisfaction of finding quick, little wins to maximize the front-end performance of a website. Lately, they've been found by digging through the modern browser's many resource hints.

One in particular has caught my fancy: DNS prefetching. It hasn't enjoyed the same spotlight as hints like preload in recent years, but it has a compelling advantage if the conditions are right – namely, if your site could possibly interact with third-party domains at any point of a page visit. Thinking through those conditions could reap satisfying gains of your own, as well as get you more familiar with DNS resolution as a whole – a key, ubiquitous component of the web. That's time well-spent. Let's look at it a little more.

DNS is Everything

There's hard truth behind the "it's always DNS" meme that flares up every time there's a widespread network outage. It's a technology foundational to the web and yet inadvertently overlooked, making it real fun to catch when things go wrong. At a conceptual level, however, it's not the craziest thing to grasp. DNS is often reduced to being the "phone book of the internet.” I think that's a great metaphor.

Let's map out a stupid-simple version of the process for a browser navigating to picperf.io:

  1. The user fires off a request to picperf.io in the browser. It looks clear enough, but the domain is actually a facade. PicPerf's origin server lives at an IP address associated with that domain. That's what the browser needs to direct the request. So, before moving forward, we gotta look it up.
  2. That lookup process begins by going to a recursive DNS server. This is a sort of mediator between a client and the server holding the domain-to-IP association. For another metaphor, think of a recursive DNS server as the detective you task with chasing down the answer to a mystery.
  3. A subset of different DNS servers are used to find the "source of truth" server containing all the records (A, TXT, CNAME, etc.). It starts by resolving through the TLD, and works it's way up to the authoritative server.
  4. The recursive DNS server kicks back the IP address to the browser. The detective has done its job.
  5. PicPerf's origin server receives a request and provides a response.

Here, I made this diagram to help visualize this.

DNS resolution diagram

What's not represented here, by the way, is the amount of caching and other complexity that occurs for lookups all the way from the browser up to the various servers. That's a gnarly rabbit hole we're not gonna bother with right now.

It Really Can Add Up

When a DNS record is in place and correctly configured, there isn't much to think about. Resolvers are smart and the process is pretty fast. But the typical website isn't just requesting resources from its own domain. There are often many, many third party domains in play, and those resolutions can add up, sometimes faster than you think.

I explored this on my own by using a Puppeteer script that loads a page, scrolls to the bottom, and adds up all the DNS resolution time for every asset it could inspect. Here are some of the results I got from a few sites after hitting them with a cold request. Bear in mind that none of this is scientific:

  • ebay.com: 6 distinct hosts, 117.47ms in total resolution time.
  • rd.com: 5 distinct hosts, 108.89ms in total resolution time.
  • msn.com: 12 distinct hosts, 306.56ms in total resolution time.
  • temu.com: 9 distinct hosts, 108.25ms in total resolution time

Granted, aggregate numbers like this don't reveal the purpose or timing of those resolutions – only that they happened at some point after the page was accessed. But it illustrates that the cost of having your site reach out to multiple domains is more than zero. Let's wrap our heads around why that is with an example.

I loaded the same image from five different domains.

<img src="https://picperf.io/img/4CAF9H/rat.jpg?1" />
<img src="https://images.macarthur.me/img/4CAF9H/rat.jpg?2" />
<img src="https://images.typeitjs.com/img/4CAF9H/rat.jpg?3" />
<img src="https://images.jamcomments.com/img/4CAF9H/rat.jpg?4" />
<img src="https://images.plausiblebootstrapper.com/img/4CAF9H/rat.jpg?5" />

When the page loads, the waterfall for those images looks something like this:

standard waterfall

Each color represents a different stage in the request lifecycle. The light blue color (second from the left) represents the DNS resolution phase. Each of these domains is distinct from the page's domain, so each needs to go through this process.

For fresh requests like this, there isn't a whole lot we can or need to do to cut down on that lookup time. The browser's preload scanner already identifies those resources when the document is parsed, so it's able to kick off the requests very quickly. It's a little more complicated for late-discovered or lazily loaded resources, though.

Get a Head Start w/ DNS Prefetching

Let's change the page up a bit. Instead of immediately loading all of those images, we'll use JavaScript to lazily append them to the page after a few a second has passed. But not necessarily all of them. We'll flip a coin to determine if it'll render from that host:

const hosts = [
  "picperf.io",
  "images.macarthur.me",
  "images.jamcomments.com",
  "images.plausiblebootstrapper.com",
  "images.typeitjs.com",
];

function loadImage(host) {
  // Randomly load the image from this host.
  if (Math.random() < 0.5) return;
  
  const img = new Image();
  img.src = `https://${host}/img/4CAF9H/rat.jpg?${Math.random()}`;
  document.body.appendChild(img);
}

setTimeout(() => hosts.map(loadImage), 1000);

Let's say that flip of a coin represents some indeterminate behavior on your page. It could be user input, network conditions, or a bajillion other things. The point is that the page might communicate with a host, but it's not guaranteed.

This time, the browser's preload scanner can't immediately begin resolving anything. Every DNS resolution needs to wait until the request begins later on. You can see that in the selected window:

waterfall with no dns-prefetching

Since there's a chance every host could be hit on any page load, we can get an affordable head start on resolving their DNS records (it's not an expensive process, so there's little to lose). We'll do that with a dns-prefetch resource hint. One hint for each distinct domain we might interact with:

<head>
  <link rel="dns-prefetch" href="https://picperf.io" />
  <link rel="dns-prefetch" href="https://images.macarthur.me" />
  <link rel="dns-prefetch" href="https://images.jamcomments.com" />
  <link rel="dns-prefetch" href="https://images.plausiblebootstrapper.com" />
  <link rel="dns-prefetch" href="https://images.typeitjs.com" />
</head>

Now, take a look at our waterfall – the "DNS Lookup" phase has been virtually wiped out.

waterfall with dns-prefetching
connection details for dns-prefetching

By the time the browser was asked to request and load the images, it already had the respective IP addresses locked & loaded. No life-altering results here, but it does feel nice to make that waterfall a little slimmer.

DNS Prefetching, Preconnecting, & Preloading

They're not in focus here, but it's worth a minute to touch base on two other resource hints often used in the same conversation as DNS prefetching: preconnect and preload. (The prefetch hint may have also come to mind, there are still gaps in browser support, and it's recommended to reach for the Speculation Rules API in place of it anyway.)

Each of these three hints go a bit further in the request lifecycle:

  • dns-prefetch: Resolve the DNS and leave the rest to the browser.
  • preconnect: Establish a full connection to the server (DNS resolution, TCP handshake, and TLS negotiation), and leave the rest to the browser.
  • preload: Perform a full fetch of the resource, start to finish.

Each come with a different cost, so choosing between them highly depends on how likely you are to need a late-discovered resource. Here's generally how you'd navigate that decision:

  • Reach for dns-prefetch if there's a chance you'll need resources from an external domain, or if your site depends on a resources from a variety of third-party hosts. The hint is low-cost and good for getting an edge up on requests in the chance they're necessary.
  • Reach for preconnect if you know the user will soon be downloading resources from a domain. Opening up that connection in advance will help the browser cut to the chase when it's time to request something. Just don't get carried away. This is a more expensive process than dns-prefetch, and there's no need to do it for resources within range of the browser's preload scanner (that is, referenced directly in your HTML).
  • Reach for preload you're dealing with specific, high-priority resources that will certainly be downloaded sometime later in the page lifecycle. Fonts referenced in CSS is the go-to example here.

Those rules aren't hard & fast, so leave room for a million different factors & circumstances. But when they're used appropriately, they can give you a nice bump.

A Taste of preconnect and preload

In light of all that, let's explore a little further. Say we know all of the images in our example would be downloaded for sure. In this case, establishing a full connection with preconnect is a good idea. Here's a slight taste of how it would've helped us out. Let's drop a hint for each distinct domain:

<link rel="preconnect" href="https://images.macarthur.me" />
<link rel="preconnect" href="https://images.jamcomments.com" />
<link rel="preconnect" href="https://images.plausiblebootstrapper.com" />
<link rel="preconnect" href="https://images.typeitjs.com" />

Those hints will clue in the browser to establish a full connection to the origin, without performing a download. It gets the DNS, TCP, and TLS steps out of the way, and our waterfall reflects that:

preconnect waterfall

In fact, neither the DNS, initial connection, nor SSL sections aren't even listed at all:

You can likely predict what'll happen if we drop an even more aggressive hint with preload. This time, we'll call out each image specifically:

<link rel="preload"
      as="image"
      href="https://picperf.io/img/4CAF9H/rat.jpg" />
<link rel="preload"
      as="image"
      href="https://images.macarthur.me/img/4CAF9H/rat.jpg" />
<link rel="preload"
      as="image"
      href="https://images.jamcomments.com/img/4CAF9H/rat.jpg" />
<link rel="preload"
      as="image"
      href="https://images.plausiblebootstrapper.com/img/4CAF9H/rat.jpg" />
<link rel="preload"
      as="image"
      href="https://images.typeitjs.com/img/4CAF9H/rat.jpg" 
/>

Now there is no delay in fetching the images. They're downloaded within a few milliseconds of page load, and are rendered to the page only when needed:

preload waterfall

One more note on preload: specifying the exact URL matters. Even if the slightest character in a query string parameter differs, the browser will treat it as a distinct resource, causing a re-download of the same asset. Watch out for that footgun.

Dream Big

Neither DNS prefetching nor other resource hints revolutionize your page performance – they're more a scalpel than a chainsaw. Regardless, it's nice to have them within reach when there's opportunity for some small but real gains. Plus, when used in concert with the several other tools out there for tuning performance, you could end up with a bigger edge than you might've expected. The impact may just snowball from there, leading to mind-blowing conversion rates, great wealth, and global renown for your impact in the front-end website performance space. You could even convince the Secretary of the Treasury to put your face on a bill.

Just dreamin' here.

Ready to upgrade your site's image performance?

Start a Free Trial (no card required)