Optimizing Assets (preload/fetch/dns-prefetch/preconnect)

Introduction

During a previous interview, I was asked about a SPA I created and its poor image loading experience when changing pages. I shared all my thoughts, which I wasn’t very confident about 😅. My answers included:

  • Images should have width and height set to avoid CLS (Cumulative Layout Shift)🔗
  • Images can add Skeleton or Loader to indicate loading
  • Initially display smaller images, then replace them with the actual larger images at the right time
  • Images can use fetchpriority to arrange resource loading priority
  • Inline SVG or data URI can be used for images
  • Use Preload to pre-load critical images…?

This time, I aim to clarify the mechanisms provided by browsers in this regard, mainly introducing how browsers handle resource loading. The <head> section is typically where HTML loads resources, and the <link> tag has a special rel attribute to indicate how external resources should be loaded.

Problem: Timing of Loading Order

Web page images only load when the page is loaded, so when changing pages, some key images not appearing can seem very odd. Aside from directly embedding resources, is there a way to change the resource loading order of the page to improve user experience?

Understanding rel="preload"

This allows you to declare requests for data to be fetched when the page loads, typically for data that you want to load immediately early in the page load:

  • Resources pointed to by CSS, such as fonts or images
  • Resources requested by JavaScript, such as JSON
  • Various large resources
  • ……

For example, when using Google Fonts, you receive a CSS file containing all the @font-face rules pointing to the download locations of the fonts. This way, you don’t have to load the entire font package at once; the browser compares the text appearing on the page with the unicode-range in this file and downloads the corresponding font subset file:

/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

If you want to “pre-download specific resources,” you can specify preload and type as="font" to have specific font files downloaded in advance.

<link rel="preload" href="https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjx4wXg.woff2" as="font" crossorigin />

Other types of Preload examples:

<link rel="preload" href="/style/other.css" as="style" />
<link rel="preload" href="/assets/font.woff2" as="font" />
<link rel="preload" href="/img/header.png" as="image" />

Understanding rel="prefetch"

In contrast to rel="prefetch", which focuses on optimizing the loading of the current page’s content, preload is mainly used for “downloading resources that may be needed on the next page.” The most obvious use is to pre-load a page that the user might be interested in next. This way, the user doesn’t have to load the next page in real-time when changing pages:

<link rel="prefetch" href="next.html" />

Since it is not very urgent for the next page’s experience, this operation is executed when the browser is idle. It is suitable for non-urgent resources that appear when changing pages or in pop-ups. Other Types of Prefetch Examples:

<link rel="prefetch" as="script" href="/date-picker.js">
<link rel="prefetch" as="style" href="/date-picker.css">

Understanding rel="dns-prefetch"

In contrast to “pre-downloading specific resources,” you can also pre-resolve the target URL’s DNS to speed up the request when it occurs.

<link rel="dns-prefetch" href="https://example.com/" as="document" />

Understanding rel="preconnect"

Tells the browser which website you want to preconnect to, usually a domain that will be used for resources later, allowing the browser to establish a connection in advance to reduce latency.

<link rel="preconnect" href="https://example.com/" crossorigin />

dns-prefetch vs preconnect

The difference between dns-prefetch and preconnect is that dns-prefetch only performs a DNS lookup, while preconnect will perform DNS lookup, TLS negotiation, and TCP handshake. This means that once the resource is ready to download, it can avoid an additional 2 round-trip communication delays. Generally speaking, preconnect does more preparation and has poorer browser support.

Summary

  • preload
    • Purpose: Tells the browser which resources will be needed later on the current page.
    • Priority: Higher, the browser will load it as soon as possible.
    • Use case: Suitable for preloading critical resources for the current page, such as critical images, critical CSS, and critical JavaScript.
  • prefetch
    • Purpose: Tells the browser which resources might be needed on the next page.
    • Priority: Lower, the browser will load it during idle time.
    • Use case: Suitable for preloading resources for linked pages that users might click on, such as preloading article content on a list page.
  • dns-prefetch
    • Purpose: Tells the browser about URLs that might be linked.
    • Use case: Pre-resolves DNS addresses to speed up loading.
  • preconnect
    • Purpose: Tells the browser which website you want to preconnect to.
    • Use case: Pre-establishes connections to reduce latency when loading resources.

It should be noted that loading and execution are independent actions, and after loading, you can set the actions to be taken during execution:

Style execution example:

<link
rel="preload"
as="style"
onload="this.rel = 'stylesheet'"
href="https://fonts.googleapis.com/css?family=Roboto:100,900|Material+Icons"
/>
<noscript>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:400,600|Material+Icons" />
</noscript>

Script execution example:

<link rel="preload" href="used-later.js" as="script" />
<!-- ... -->
<script>
const usedLaterScript = document.createElement('script');
usedLaterScript.src = 'used-later.js';
document.body.appendChild(usedLaterScript);
</script>

Further Reading