SOSOLOG

A Closer Look at index.html - Part 1

February 13, 2020

Translated from the Korean original.

image-thumbnail

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.

Parts

TL;DR

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.

preload

<link rel="preload"> tells the browser: I need this resource right now, so fetch it as fast as possible.

rendering

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:

  1. The browser requests the HTML file.
  2. It starts parsing the HTML response and building the DOM.
  3. It finds CSS, JS, and other resources along the way and fires off requests for them.
  4. Once all the CSS has arrived, it builds the CSSOM and merges it with the DOM tree into a render tree.
    • Only now does the render tree know which fonts it actually needs, so the font request goes out.
  5. It runs layout and paints the content to the screen.

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.

warning

preconnect

<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:

  1. 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

  2. If it’s valid, treat that URL as the origin

  3. Assign the cors state to the target element’s crossOrigin attribute

  4. Attempt the connection if crossOrigin is anonymous, or if credentials aren’t set to false

  5. 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

preconnect

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

preconnect-vs

You can request Google’s CSS and font from the same domain at once, which ends up eliminating 3 round trips.

prefetch

<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

prefetch

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.

font

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.

script - async / defer

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.

The usual approach

without-defer-async-head without-defer-async-body

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.

async

with-async

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.

defer

with-defer

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)?

SPA

An SPA doing client-side rendering draws its screen by parsing JS. Say we have two JS files:

<!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>

async

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

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.

When should you actually use them

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.

Reference


SO_YOUNG
👋@SO_YOUNG
📝 A small, casual dev log

GitHubX (Twitter)