February 13, 2020
Translated from the Korean original.
![]()
When you build with a library or framework like React, Vue, or Angular, the index.html in your build output gets generated automatically, so you may never actually open it.
But knowing exactly what goes into that final HTML pays off once you’re chasing a performance win or hunting down a bug.
The link tag describes the relationship between a page and an outside resource. You’ll see it most with css, but it also loads site icons, sitemaps, and scripts.
Beyond the basics, you can add a media attribute — set one, and the css only loads once that condition holds.
<link href="print.css" rel="stylesheet" media="print"><link href="mobile.css" rel="stylesheet" media="screen and (max-width: 600px)">Depending on the option you pick, a resource can jump ahead of others and load early, or the browser can open a connection before it’s even needed.
<link rel="preload"> tells the browser: I need this resource right now, so fetch it as fast as possible.

Fonts are the classic use case.
<link rel="preload" as="font" crossorigin="crossorigin" type="font/woff2" href="myfont.woff2">Request a font without preload and text rendering can end up delayed, since the font request doesn’t fire until after the DOM and CSSOM trees exist. Here’s the order a browser paints a page in:
For content that has to show up the instant the render tree is ready, this sequence can make the font visibly show up late.
Every browser implements this a little differently. See Web Font Optimization for the details.
That’s exactly why essential resources deserve a priority boost via preload. Preload a font and the request goes out without waiting for the CSSOM to finish building.
Leave a resource you preloaded unused for more than 3 seconds, and Chrome DevTools calls you out with a warning like this.

<link rel="preconnect"> opens a connection to the server ahead of time, before any HTTP request actually needs it.
When you’re requesting a resource from a different domain, you can’t count on that server responding quickly. And when a secure connection is required, just setting up the connection — DNS lookup, redirects, the TCP handshake — can take longer than actually receiving the data.
preconnect is how you tell the browser to get that connection out of the way early. Here’s how you use it.
<link rel="preconnect" href="https://example.com">Under the hood, here’s what actually happens:
Parse the URL from the href attribute, check whether it’s valid (erroring out if not), and figure out whether it’s HTTP or HTTPS
If it’s valid, treat that URL as the origin
Assign the cors state to the target element’s crossOrigin attribute
Attempt the connection if crossOrigin is anonymous, or if credentials aren’t set to false
Run DNS+TCP for http (or DNS+TCP+TLS for https), then leave the connection open — the user agent itself decides how many connections to keep around

Using preconnect cuts out a round trip entirely, and that alone saves a meaningful chunk of time.

You can request Google’s CSS and font from the same domain at once, which ends up eliminating 3 round trips.
<link rel="prefetch"> waits until every high-priority resource has been requested, then fetches whatever’s left over during idle time and drops it into the browser cache.
Which makes it a poor fit for anything the first page needs right away.
prefetch comes in three flavors.
1. Link Prefetching

As just described, prefetching fetches a resource and stashes the result in the browser cache.
“This technique has the potential to speed up many interactive sites, but won’t work everywhere. For some sites, it’s just too difficult to guess what the user might do next. For others, the data might get stale if it’s fetched too soon. It’s also important to be careful not to prefetch files too soon, or you can slow down the page the user is already looking at. - Google Developers”
In other words: think carefully about which resources are actually worth prefetching. Use it carelessly and you’ll slow down the very page the user is looking at.
Worth checking browser support before you reach for it, too.
2. DNS Prefetching
This just runs a DNS lookup in the background.
By the time you actually need the resource, the DNS lookup is already done, so it loads faster.
Here’s how you use it.
<!-- Prefetch DNS for external assets --><link rel="dns-prefetch" href="//fonts.googleapis.com"><link rel="dns-prefetch" href="//www.google-analytics.com"><link rel="dns-prefetch" href="//cdn.domain.com">3. Prerendering
prerendering shares prefetch’s basic idea: fetch something ahead of time in case it turns out to be needed.
The difference is that prerendering actually renders the entire page in the background.

Prerender a resource nobody ends up needing, and you’ve just wasted bandwidth.
Note. crossorigin
Request an external resource without a crossorigin attribute, and the browser discards what it already loaded and applies a freshly fetched copy with different attributes instead. This is required for cross-domain requests, and it takes one of these values.
When the parser hits a script tag while parsing HTML, everything stops. css is placed in the head since it’s usually essential to rendering, but stalling the render just to load JavaScript that handles ‘behavior’ isn’t a great experience for users.
That’s why most script tags end up declared right before </body>. But there’s more than one way to handle this — you can also reach for async or defer.

As the images above show, a script tag blocks HTML parsing.
So put a script tag in the head, and users end up staring at a blank screen longer. Moving it to the very bottom of the body avoids that.
This is the fallback for browsers that don’t support async or defer.

The async attribute does nothing unless the script actually sits in the head.
Behaves exactly as if you hadn’t added it at all.
The script is requested asynchronously, and the moment the fetch completes, HTML parsing pauses while the script runs. Parsing resumes once it’s done.

Like async, the script downloads asynchronously, but it only runs after HTML parsing finishes. Since it never blocks parsing, the screen can render sooner.
So how do these two attributes play out in a Single Page Application (SPA)?
An SPA doing client-side rendering draws its screen by parsing JS. Say we have two JS files:
src that actually draws the screen<!DOCTYPE html><html lang="ko"><head> <title>SPA - script</title></head><body> <div id="wrap"></div> <script src="external.js"></script> <script src="app.js"></script></body></html>external.js gets requested, then app.js right behind it. The instant external.js finishes loading, it runs immediately and pauses HTML parsing. If app.js also finishes loading while parsing is still underway, the same thing happens: parsing pauses and the script runs.
With two or more scripts in flight like this, async gives you no guarantee about order, so it’s best kept for scripts that have nothing to do with the DOM.
defer fetches both scripts asynchronously too. The difference is that while defer requests vendor.js and app.js asynchronously, it still guarantees the order they run in.
Both defer and async cut down script request time. But an SPA’s HTML parsing was never taking long to begin with, so don’t expect either one to deliver a dramatic performance win.
That’s because, in the example above, the screen only appears once app.js has run, and an SPA’s HTML is a barebones structure to begin with.
If anything, requesting a DOM-manipulating script through async — where execution order isn’t guaranteed — is a good way to introduce bugs. Save async for scripts with no dependencies, like an analytics script such as GA.