Get All That Network Activity Under Control with Priority Hints
Alex MacArthur
Posted on September 19, 2023
Open up the browser's network tab and you'll see a lot of activity. Assets are being downloaded, information's being submitted, events are being logged, and more.
With so much going on, effectively managing the priority of that traffic is pretty critical. Bandwidth contention is real, and some HTTP requests simply don't rank as highly as others when they're all firing at once. For example, if you had to choose, you'd probably prefer someone's payment request successfully complete vs. an analytics request simply indicating they tried. And having your hero image show up as quickly as possible is arguably more important than your logo rendering in the footer of your page.
Fortunately, the browser has a growing collection of tools to help prioritize all this network activity. These "priority hints" help the browser make fewer assumptions and clearer decisions about which requests to favor over others when resources are limited.
It's a useful suite of tools, and they can make a tangible impact to page performance when leveraged well, including those increasingly important core web vitals. Let's explore a few of them, along with some scenarios in which they're most helpful.
Prioritizing Preloaded Assets
Modern browsers have a well-supported means of being told about resources that'll eventually be needed on the current page: <link rel="preload" ... />
. When placed in the <head>
of the document, the browser is instructed to begin downloading it as soon as possible with "high" priority.
To be fair, the preload scanner in the browser is already really good at this sort of thing (I've previously written about it in a little more depth). For that reason, preloading is best used on late-discovered assets – anything not loaded directly by your HTML. Think: a font file loaded within some CSS, or a background image loaded via an inline style
attribute.
Consider a font that's loaded exclusively through a CSS @font-face
rule:
@font-face {
font-family: "Inter Variable";
src: url("./font.woff2") format("woff2");
}
On load, that font gets the lowest download priority, despite it being pretty important for the visual experience of the page.
Now, let's preload that resource:
<head>
<!-- Other stuff... -->
<link rel="preload" href="/font.woff2" as="font">
</head>
It's now far more favored:
But you can get even more explicit using the fetchpriority
directly on the link
tag. It'll let you signal relative priority when preloading multiple assets at once.
Here's a contrived scenario in which you'd like to preload two fonts, but give one priority over the other:
<link rel="preload" href="./font-1.woff2" as="font" fetchpriority="low" />
<link rel="preload" href="./font-2.woff2" as="font" fetchpriority="high" />
The resulting network activity reflects these instructions (I included a third, non-preloaded font in there too, just because).
When to Use It
In general, use preloading when an asset isn't loaded by your HTML directly, but it's critical to the experience of the page (fonts, CSS background images, etc.). And include the fetchpriority
attribute when preloading several resources of the same type and you're clear about which is most important.
Prioritizing fetch()
Requests
In my opinion, the Fetch API is one of the most ergonomic offerings of the modern web. And it has some nice features that XMLHttpRequest
lacked, like the ability to signal priority on an outgoing request.
The most top-of-mind use case is one I've already mentioned: analytics requests. When bandwidth is slim and multiple requests are in play, the browser will make its own priority decisions. But we as engineers (should) know that your typical analytics requests ought to take a back seat to others more critical to the page's purpose. Modern fetch()
makes that easy.
Here's a simple setup with two requests being queued at (virtually) the same time:
fetch("http://localhost:8000/pay", {
method: "POST",
body: paymentBody,
});
fetch("http://localhost:8000/log", {
method: "POST",
body: loggingBody,
});
By default, the browser will automatically consider them both "high" priority:
Now, we'll explicitly tell the browser how each request should be prioritized:
fetch("http://localhost:8000/pay", {
method: "POST",
body: paymentBody,
+ priority: "high"
});
fetch("http://localhost:8000/log", {
method: "POST",
body: loggingBody,
+ priority: "low"
});
This time around, the priorities are different:
Useful counterpart: keepalive: true
A possible concern here is that "low" priority requests may be lost to the void – cancelled if the user navigates away from the page too soon. That's a legitimate issue. Depending on a few factors, either closing the tab or moving onto the next page may cause an important, but relatively low-priority request to be aborted.
Fortunately, fetch()
also accepts a keepalive
option. When set to true
, the browser will carry that request to completion even when the page is terminated. If you'd like to dig into it more, take a gander at what I wrote for for CSS-Tricks a while back.
When to Use It
Indicate fetch()
priority when you know multiple requests are executing concurrently, and it's clear to you which is most important (or which can safely be deprioritized).
Prioritizing <img />
Requests
Without us doing anything special, the browser attempts to do its best at determining the most important images on a page. To illustrate this, I loaded the following images, spaced a good distance apart, so only one would be "above the fold."
<img src="./cat-1.jpeg" />
<div style="height: 5000px"></div>
<img src="./cat-2.jpeg" />
<div style="height: 5000px"></div>
<img src="./cat-3.jpeg" />
The browser picked up on which was most important, but it took a second. When downloading began, all three were "low" priority. But shortly after, the one above the fold switched to "high."
Things became more predictable when I added a fetchpriority
attribute to the first image:
<img src="./cat-1.jpeg" fetchpriority="high" />
After that, cat-1.jpeg
loaded at the highest priority right out of the gate. While initially head-tilting, it makes sense. The browser is smart at determining resource criticality, but it benefits from clear instructions. If you know an image is important, be explicit about it.
This feature, by the way, pairs very nicely with native image lazy loading, a very well-supported feature these days.
<img src="./cat-1.jpeg" fetchpriority="high"/>
<div style="height: 5000px"></div>
<img src="./cat-2.jpeg" loading="lazy" />
<div style="height: 5000px"></div>
<img src="./cat-3.jpeg" loading="lazy" />
With that in place, the browser knows exactly how (and if) to load images, only when appropriate. In my case, it won't even begin the request for off-screen images on initial load. Instead, it'll wait until they're closer to the viewport.
When to Use It
Use an explicit fetchpriority
on images when you know they're very important to page experience. Hero images are a great place to start, and it can even have an impact on a page's core web vitals – specifically, LCP (largest contentful paint).
Prioritizing <script />
Tags
Any plain <script />
with a src
attribute on a page will get high
priority as it's being fetched, but there's a trade-off: it blocks parsing of the rest of the page until it's loaded and executed. For that reason, the async
attribute is helpful. It'll request the script in the background at low
priority, and execute as soon as it's ready. Knowing this, the following setup behaves predictably:
<script src="/script-async.js" async onload="console.log('async')"></script>
<script src="/script-sync.js" onload="console.log('sync')"></script>
<script>console.log("inline");</script>
The asynchronous script is demoted in priority:
And the console confirms that subsequent scripts were allowed to parse and execute as the async
script was loading.
Non-Blocking, but High-Priority Scripts
Most of the time, that behavior's fine. But sometimes, you might want a script to load both asynchronously and with "high" priority.
A possible scenario is a small SPA mounted in the hero of a landing page. In order to preserve the page's core web vitals, specifically LCP and FID (first input delay, soon to be replaced by interaction to next paint), you'll need that script to be highly prioritized (after all, it's responsible for building and powering your application). But at the same time, you don't want it to block the rest of the page from parsing.
So, let's give it a fetchpriority
:
+ <script src="/script-async.js" async onload="console.log('async')" fetchpriority="high"></script>
<script src="/script-sync.js" onload="console.log('sync')"></script>
<script>console.log("inline");</script>
And now, it's downloaded with elevated priority, while still not blocking the rest of the page:
And the console verifies this. With that higher priority, the async script loads more quickly. In this case, even before the synchronous and inline ones.
While I didn't toy with it specifically here, yes, fetchpriority
works with deferred scripts as well.
When to Use It
Place fetchpriority
on your scripts when you know their priorities up front, and if you suspect the browser won't have enough information to determine on its own. As I mentioned, it's particularly helpful for prioritizing scripts that you'd also like to load in non-blocking, asynchronous way.
To Be Used Intentionally
It's easy to become a little overzealous about tools like this, leading to overuse. So, be careful – doing so may come at a cost. As the adage goes: "emphasizing everything = emphasizing nothing." In fact, overuse may actually make it more difficult for the browser to manage against network contention, harming a page's performance.
MDN even makes a point to call it out in some of their priority hint documentation:
Use it sparingly for exceptional cases where the browser may not be able to infer the best way to load the resource automatically. Over use can result in degrading performance.
So, don't feel obligated to reach for these tools just because they exist. Wield them with care.
Reviewing: When to Hint
There's a lot here, so let's quickly review some times you might choose to leverage priority hints. They're not exhaustive. Just some good places to start.
- Hint preloaded assets when you want the browser to be aware of multiple late-discovered resources, some of which bring more critical to the page than others.
- Hint
fetch()
requests that you know are either a crucial part of a user's experience, or can safely be deprioritized to make way for more important ones. - Hint above-the-fold images you want to be loaded & visible as soon as possible.
- Hint scripts that are key to the function of a page, but you don't want to block the rest of the page (including other assets) from being parsed and downloaded.
Make the Browser Guess Less
The browser is incredibly smart about figuring out how and when to download the stuff that makes our pages tick. But it's not always great. It doesn't know why a page exists or the intent behind its individual parts. And so occasionally, it could use some extra help.
That's why these priority hints exist: to make instructions clear, and to leave the browser very little chance to get those decisions wrong. Keep them in mind the next time you're digging through your own application's network activity, and when it makes sense, reach for them to help make your page performance just a little more intelligent.
Posted on September 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.