There's an abundance of browser tools that help you signal which assets have the highest or least priority when you're loading them for a page. Some have been around for quite a while, like the async
and defer
attributes on <script>
tags. Others, however, are still relatively new to the scene, albeit very widely supported and used.
One of them is <link rel="preload">
. Introduced around 2016, preloading allows you to tell the browser that a specific resource will definitely be used on the page, so it should go ahead and download it now with elevated priority.
It's a great tool. But since its arrival, there's been a lot of this going on: preloading an asset just above where that asset is linked.
<head>
<!-- other stuff -->
<link rel="preload" href="https://example.com/script.js" as="script">
<script src="https://example.com/script.js"></script>
</head>
In English: "The HTML document is already poised to download a resource, but we're gonna go ahead and explicitly preload it anyway."
The rationale makes some sense. If a file is critical to the experience of the page, there's an incentive to tell the browser it's really important to prioritize it.
That can be useful, but if you stop there, you're leaving a lot of value on the table, and risk overlooking the bigger problem it was designed to solve. Let's explore.
Loading Assets without Preloading
I set up a simple HTML document with a few resources being loaded in the <head>
, along with some images and a script in the <body>
.
<head>
<link rel="stylesheet" href="http://localhost:8080/style.css">
<link rel="stylesheet" href="http://localhost:8080/style2.css">
<link rel="stylesheet" href="http://localhost:8080/style3.css">
</head>
<body>
<img src="https://placekitten.com/g/1201/1201" />
<img src="https://placekitten.com/g/1202/1202" />
<img src="https://placekitten.com/g/1203/1203" />
<script src="http://localhost:8080/script.js"></script>
</body>
The first CSS file also loads a .woff2
font file via a @font-face
rule, but obviously, only after it's been parsed by the browser.
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(http://localhost:8080/font.woff2) format('woff2');
}
On page load, resources begin to load fairly predictably, mostly in order of document position.
Because they're embedded directly in the document itself, the majority load in "medium" to "highest" priority (more details on the distinction between those levels here). And as a sub-resource called from style.css
, the font it gets lowest priority.
What's most interesting about this, however, is that despite being marked "medium," our JavaScript begins loading before any of the images. It's like the browser is up to something.
The Impact of Preloading
Now, let's revisit the idea of preloading a really important script already called on the page. We'll throw a new line in there, right at the top.
<head>
+ <link rel="preload" href="http://localhost:8080/script.js" as="script">
<link rel="stylesheet" href="http://localhost:8080/style.css">
<link rel="stylesheet" href="http://localhost:8080/style2.css">
<link rel="stylesheet" href="http://localhost:8080/style3.css">
</head>
<body>
<img src="https://placekitten.com/g/1201/1201" />
<img src="https://placekitten.com/g/1202/1202" />
<img src="https://placekitten.com/g/1203/1203" />
<script src="http://localhost:8080/script.js"></script>
</body>
Here's how that network waterfall looks now:
By the looks of it, there's been some impact. The script is now the first asset to begin downloading. But the benefit is marginal. You can't tell from the screenshot, but it started downloading a mere 16 milliseconds before the last image did. And as you can see, the assets (except the sub-requested font) still appear to be loading concurrently.
But it's also graduated to "high" priority. That's good, but at the very least, you might've expected the browser to make it "highest" priority. Instead, our styles still get the top rank. Regardless of where it was placed in the document, the browser just seems to already know how to handle the resource relative to others' positioning and type. There's a bit of benefit to preloading, but nothing impressive. Maybe even not worth bothering with at all.
The Preload Scanner's on the Job
If you're expecting huge gains from rel="preload"
resources called in your HTML, you're setting yourself up for disappointment, because the browser is already good at what we often want preload
to do. That's because of a built-in mechanism called the "preload scanner."
The preload scanner is a secondary parser running while any document loads. Its purpose is to look ahead in the document, identifying any resources in the HTML that it can start fetching as soon as possible. That includes any sort of normal asset loaded via HTML tag β <img>
, <link>
, <script>
, etc.
And that means that no matter where that <script>
tag is placed, the browser will always find it and always give it reasonable priority when loading, contextual to the other assets it knows need to be loaded too.
Of course, that behavior assumes a few things:
- You're not using
defer
orasync
attributes. - The tag is embedded in the HTML itself (and not injected via JavaScript)
- The HTML isn't rendered by a JavaScript framework, like React.
So, the browser is smarter than you might think. But that doesn't mean preloading isn't an effective tool when used in a better way.
Better Preloading: A Seeing Eye Dog for the Browser
Rather than preloading assets already embedded in your HTML, a better use case is using it to prioritize resources that you know will be needed later on, but are being requested by assets not yet loaded themselves (you'll often see these referred to as "late-discovered resources"). Put another way, preloading can be thought of as a seeing eye dog to the brower's preload scanner.
A great example is our low-priority font file from above. We know it's gonna be needed (we wrote the CSS that loads it), so let's try preload it.
<head>
+ <link rel="preload" href="http://localhost:8080/font.woff2" as="font">
<link rel="stylesheet" href="http://localhost:8080/style.css">
<link rel="stylesheet" href="http://localhost:8080/style2.css">
<link rel="stylesheet" href="http://localhost:8080/style3.css">
</head>
<body>
<img src="https://placekitten.com/g/1201/1201" />
<img src="https://placekitten.com/g/1202/1202" />
<img src="https://placekitten.com/g/1203/1203" />
<script src="http://localhost:8080/script.js"></script>
</body>
If you remember, that font file was previously fetched last and in low priority because it was requested only after our CSS was downloaded and parsed. Now, however, things look different.
Despite being a sub-resource, our font now has elevated priority.
The font file is now the first resource to be fetched, and it has a "high" priority, giving a huge head start on downloading before it's actually needed. By the time that "lowest" priority font request hits, the file is already cached and ready to go. In this scenario, we're letting the preload scanner do what it does best, and leveraging preloading to let the browser know about the resources it can't yet see.
Maximizing Preload Impact
Here's what I'm not saying: "There's literally zero value to preloading anything already being loaded by your HTML." We've seen how that's not necessarily the case. Instead, here's the big takeaway: if you're stopping at preloading HTML-embedded resources, you're missing out on its real power.
With that in mind, here are two bits of guidance as you dig into your HTML:
First, keep your eyes peeled for resources that later fetch more resources themselves. We've seen it with our example CSS above, and some widely known resources out there do it too. For example, Google Fonts asks you to drop a CSS file on a page, which only turns around to request font files. The preload scanner can't see those in advance, and you should definitely preload things like this.
But JavaScript is another big culprit too. There's a world of pure, single-page applications out there, and none of them can fetch anything until JavaScript is able to download, parse, and execute. Addy Osmani drive this home really well. In these cases, the preload scanner is completely blind to critical static assets, and declaratively preloading them will yield big gains.
Performance tooling touches on this as well. If you've run a Lighthouse report and saw a suggestion to preload an image, you've seen this little disclaimer:
Don't miss it. Preloading is most useful "if the LCP element is dynamically added to the page." That is, if JavaScript slaps it on the page after the preload scanner's work is already done.
Second, be aware of what's not caught by the preload scanner. In particular, I'm thinking of inlined background images on elements:
<div style="background-url: url('https://whatever/image.jpg');">
Some content.
</div>
Assets like this are handled by the CSS parser, and are therefore out of the preload scanner's scope. Even if they're hard-coded into your server-rendered HTML, it's possibly in your best interest to deliberately preload those images.
Finally, don't get into the habit of preloading everything. We've already established that doing it for some assets really doesn't buy you much. And if you overdo it, you'll actually harm performance. As these smart people put it:
"Preloading too much JavaScript during startup can carry unintended negative consequences if too many resources are contending for bandwidth."
When used sparingly, however, and for just the right resources, it can be very impactful. Even more so than just prioritized your already-embedded assets.
Care about front-end performance? Check out PicPerf.dev. It's a new service for making your site's faster by automatically optimizing, reformatting, and aggressively caching a site's images, just by prefixing its URLs.
Top comments (0)