Loading CSS, JS & Resource Hints

How you load stylesheets and scripts determines whether the browser can paint the page quickly. `defer`, `async`, `type="module"`, and resource hints let you take control of the loading timeline without changing a line of application logic.

HTML8 min readConcept 39 of 42

What happens when the browser encounters CSS and JS

When the browser parses an HTML document and encounters <link rel="stylesheet">, it stops rendering and downloads the stylesheet. It must build the CSS Object Model (CSSOM) before it can calculate styles and paint anything. This is called render-blocking. A stylesheet in <head> blocks the entire first paint until it downloads and parses.

A plain <script src="..."> is even more disruptive. It is both render-blocking and parser-blocking: the browser stops parsing the HTML entirely, downloads the script, executes it, and only then continues. This is because scripts may call document.write() which would add content to the document. The traditional workaround was to put all scripts just before </body> so the HTML was mostly parsed before the script blocked. Modern solutions are the defer and async attributes.

Why the loading order affects Core Web Vitals

Largest Contentful Paint (LCP) measures how long it takes for the largest visible content element to appear. A render-blocking script in <head> delays LCP directly. An unpreloaded hero image delays LCP because the browser does not discover it until it parses the HTML. A Google Font loaded with a <link> in <head> blocks the render while the DNS resolves, the connection establishes, and the font file downloads.

The defer attribute on a script tag tells the browser: download this file in parallel while parsing continues, then execute it after the HTML is fully parsed, in document order. This is safe for almost every script that touches the DOM. The async attribute says: download in parallel, execute immediately when done -- in no guaranteed order. Use async only for independent scripts like analytics pixels that do not depend on each other or on the DOM being ready. type="module" scripts are automatically deferred.

How to use defer, async, and resource hints

Place defer on every <script> that touches the DOM and has no dependency on execution order with other scripts. Place async on fully independent third-party scripts (analytics, ad tags) that should not delay the page even if the network is slow. Use type="module" for ES module scripts; they defer automatically and support import statements.

Use <link rel="preload" href="critical.woff2" as="font" type="font/woff2" crossorigin> to fetch fonts at top priority before the CSS even requests them. Always set the as attribute to match the resource type or the browser will ignore the hint. Use <link rel="preconnect" href="https://fonts.googleapis.com"> to establish the connection to third-party origins early. Add <link rel="dns-prefetch" href="https://fonts.googleapis.com"> as a fallback for browsers that do not support preconnect.

For Google Fonts specifically, the recommended pattern is: <link rel="preconnect" href="https://fonts.googleapis.com"> then <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> before the <link> to the font CSS. This eliminates the DNS and TCP round-trips from the critical path.

Try it

Watch the load timeline for three script-loading strategies side by side.

Script loading timeline

Parse HTML
Download
Execute
Block + Download
Plain <script>Blocking

Parser blocks on download + execute

Parse HTML
Block + Download
Execute
Continue parse
deferRecommended

Parallel download, execute after parse

Parse HTML
Download
Execute
asyncIndependent only

Parallel download, executes immediately (out of order)

Parse HTML
Download
Execute
Continue

Resource hints

rel="preload"Fetch critical resource NOW at top priority

Requires as= attribute. Fonts need crossorigin.

rel="prefetch"Low-priority fetch for the NEXT page

Browser uses idle time. Good for paginated content.

rel="preconnect"Establish TCP+TLS to a third-party origin early

Use for critical external origins (fonts, API). Max 2-3.

rel="dns-prefetch"Resolve DNS for an origin without full connection

Lighter fallback for older browsers. Pair with preconnect.

Recommended Google Fonts + module pattern

<!-- Preconnect to third-party origins early -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Then the font CSS request -->
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet">

<!-- Preload the LCP hero image -->
<link rel="preload" href="/hero.webp" as="image">

<!-- ES module: automatically deferred, strict mode -->
<script type="module" src="/app.js"></script>

<!-- Third-party analytics: truly independent, use async -->
<script async src="https://analytics.example.com/a.js"></script>

type="module" is automatically deferred and supports import. Use it for your own app scripts. Use async only for truly independent third-party tags.

Check yourself

Pick an answer to lock it in, then read why. Getting one wrong is part of how it sticks.

  1. 1Script A uses defer and Script B uses async. Both start downloading at the same time. Script B finishes downloading first. In what order do they execute?
  2. 2Why must <link rel='preload'> always include the as attribute?
  3. 3A script with type='module' is automatically what?

Remember this

  • Plain <script> is parser-blocking. Place scripts before </body>, or use defer or async.
  • defer: parallel download, executes in order after HTML parsed. Use for most scripts.
  • async: parallel download, executes immediately when done (out of order). Use only for independent scripts.
  • <link rel="preload" as="...">: high-priority early fetch. The as attribute is required.

Done with this concept?

Mark it complete to track your progress. No login needed.