Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~20 min· topic 5 of 7

Topic 6.5

Linking CSS, Scripts and Fonts

In one line

How you link stylesheets, scripts and fonts decides how fast a page appears: defer, async and type="module" change when scripts run, and preload, preconnect and prefetch tell the browser what to fetch early.

Think of it like this

Cooking dinner for guests. You put the rice on to boil before chopping vegetables, so both are ready together (that's preload). You phone the shop to say you're coming before you leave home, so they open the door when you arrive (preconnect). You buy tomorrow's bread on today's trip because you'll need it next (prefetch). And you don't stop chopping every time the kettle boils; you finish chopping, then pour the tea (defer).

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Parser-blocking
Stops the HTML parser until it's finished. A plain <script src> does this.
defer
A script attribute: download now in parallel, run after the HTML is parsed, in order.
async
A script attribute: download in parallel, run the moment it arrives, in any order.
Module
A JavaScript file that uses import and export. Loaded with type="module", deferred by default.
DOMContentLoaded
The event fired when the HTML is fully parsed and deferred scripts have run (images may still be loading).
Resource hint
A <link> telling the browser to fetch or connect early: preload, preconnect, prefetch, dns-prefetch.
Handshake
The messages two computers exchange to open a secure connection before any file is sent.
Above the fold
The part of the page visible without scrolling.
woff2
The compressed font file format made for the web. Use it for all web fonts.

Step by step

01Link the stylesheet in the head

The main CSS goes in the head, before scripts. A print stylesheet can carry media="print" so it never delays the screen.

index.htmlwhole filehtml
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/print.css" media="print">

02See the three script timelines

The difference between plain, async and defer is when the download and the run happen relative to parsing.

See the three script timelinesdiagram
Rendering diagram…

03Choose per script

Your app code: defer (or type="module"). An analytics tag that touches nothing else: async. Almost nothing should be a plain blocking script any more.

index.htmlwhole filehtml
<script src="/js/app.js" defer></script>
<script type="module" src="/js/main.js"></script>
<script src="https://analytics.example/a.js" async></script>

04Connect early to other sites

If the page will load files from another origin (another domain), preconnect saves the DNS, TCP and TLS round trips. Keep it to the two or three most important origins; each open connection costs the browser something.

index.htmlwhole filehtml
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap">

05Preload what is discovered late

A self-hosted font is found only after the CSS is downloaded and parsed. Preloading it starts the download right away. The crossorigin attribute is required for fonts, even from your own site, or the browser downloads the font twice.

index.htmlwhole filehtml
<link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>

06Prefetch the likely next page

On a sign-up page, the next page is almost always the welcome page. prefetch downloads it at low priority into the cache, so the next click is instant.

signup.htmlwhole filehtml
<link rel="prefetch" href="/welcome">

Try it yourself

  1. 1

    Swap two deferred scripts

    In the first example, move the module script above the defer script. Predict the new order of lines 3 and 4, then run.

  2. 2

    Make the module async

    Add async to the module script: <script type="module" async>. Run it and see where its line lands. An async module no longer waits its turn in the deferred queue: it runs as soon as it (and its imports) are ready, so its position depends on timing and is not guaranteed.

  3. 3

    Turn the print sheet on

    In the second example, change media="print" to media="screen". Predict the heading colour first. Which stylesheet wins now, and why?

Code & diagrams

The order scripts really run in New tab

The defer script uses a `data:` URL so it has a real `src` without needing a network. Deferred scripts and modules run after parsing, in the order they appear, then DOMContentLoaded fires.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

1. plain script in the head
2. plain script at the end of the body
3. defer script
4. inline module script
5. DOMContentLoaded
Stylesheets with and without media New tab

Both sheets downloaded, but the print one applies only when printing. Try your browser's print preview on a real page to see such a sheet take effect.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Screen colour: rgb(220, 20, 60)
Print sheet applies now: false
A fast, complete head for a real sitehtml
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Asha's Kitchen</title>

  <!-- connect early to the font CDN -->
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <!-- the hero image is the largest thing on screen: fetch it first -->
  <link rel="preload" href="/img/hero.avif" as="image" fetchpriority="high">
  <!-- self-hosted font used above the fold -->
  <link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>

  <link rel="stylesheet" href="/css/main.css">
  <script type="module" src="/js/main.js"></script>
  <script src="https://analytics.example/a.js" async></script>
</head>

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

async scripts that depend on each other

Load <script src="/js/lib.js" async> and then <script src="/js/app.js" async>, where app.js uses a function from lib.js.

terminal
$ Console (sometimes)
── what you'll see ──
Uncaught ReferenceError: makeChart is not defined

Break #2

Preloading a font without crossorigin

Write <link rel="preload" href="/fonts/inter.woff2" as="font"> without crossorigin.

terminal
$ Console
── what you'll see ──
A preload for 'https://example.com/fonts/inter.woff2' is found, but is not used because the request credentials mode does not match. Consider taking a look at crossorigin attribute.

Break #3

Preloading things you don't use

Preload five images and three fonts "just in case".

terminal
$ Console
── what you'll see ──
The resource https://example.com/img/banner.webp was preloaded using link preload but not used within a few seconds from the window's load event.

Myth vs fact

Myth

async and defer are the same thing.

Fact

Both download in parallel, but defer waits for parsing to finish and keeps order; async runs immediately when ready, in any order.

Myth

Preloading everything makes the page faster.

Fact

Preload raises priority. If everything is high priority, nothing is; unused preloads waste bandwidth and the console warns about them.

Myth

Adding defer to an inline script delays it.

Fact

defer and async are ignored on classic scripts without src. Use type="module" or a DOMContentLoaded listener for inline code.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Chromium assigns fetch priorities by type and position: head CSS and fonts are highest, scripts in the head high, images low until layout finds them in the viewport. fetchpriority="high" on the LCP (Largest Contentful Paint) image is one of the most effective one-line speed fixes.

  • ▸

    Preload has an imagesrcset/imagesizes pair, so a preloaded responsive image matches what <img srcset> will pick. Without it, you may preload the wrong size and download twice.

  • ▸

    For instant navigations, Chromium supports the Speculation Rules API (<script type="speculationrules">) to prefetch or even prerender likely next pages; it replaces the old rel="prerender". Other browsers ignore it, so it is safe progressive enhancement.

  • ▸

    A 103 Early Hints HTTP response lets the server send Link: <...>; rel=preload headers before the page itself is ready, so preloads start while the server is still building the HTML (see /system-design for HTTP and caching).

Remember this

  1. 1

    <link rel="stylesheet" href="/styles.css"> loads CSS. Stylesheets in the head are render-blocking: the browser won't paint the page until they've loaded, so the page never flashes unstyled. That is good for correctness and bad for speed if the CSS is big or slow. A media attribute makes a stylesheet conditional: media="print" CSS is still downloaded, at low priority, but doesn't block the first paint. Several <link> stylesheets apply in the order they appear; later ones win ties (the CSS course covers the cascade).

  2. 2

    A plain <script src="app.js"> is parser-blocking: the parser stops, waits for the download, runs the script, then continues. defer downloads the script in parallel with parsing and runs it after the document has been parsed, in the order the scripts appear, just before the DOMContentLoaded event (the moment the page's HTML is fully parsed). Use defer for your own app code.

  3. 3

    async also downloads in parallel, but runs the script as soon as it arrives, possibly in the middle of parsing and in any order. Use it for independent scripts that don't touch your page or depend on each other, like analytics. Both defer and async work only on scripts with a src; on an inline script they do nothing.

  4. 4

    <script type="module"> loads a JavaScript module (a file that can import and export code). Modules are deferred by default, run in strict mode, have their own scope (their variables aren't global), are fetched with CORS rules and run only once even if included twice. Add async to a module to run it as soon as it and its imports are ready. Older code also used <script nomodule> as a fallback for browsers without module support; every current browser supports modules.

  5. 5

    Resource hints tell the browser about work it hasn't discovered yet. rel="preload" fetches a file needed by this page soon, with high priority; as is required (style, script, font, image, fetch) and fonts also need crossorigin. rel="modulepreload" does the same for modules and their imports. rel="preconnect" opens the connection (DNS lookup, TCP and TLS handshakes) to another site early; rel="dns-prefetch" only does the DNS step, cheaply. rel="prefetch" downloads at low priority something the next page will need. fetchpriority="high" or "low" on a link, image or script adjusts priority.

  6. 6

    Fonts are a common slowdown: the browser only discovers a font after it has the CSS that uses it. For Google Fonts, preconnect to https://fonts.googleapis.com and https://fonts.gstatic.com (the second with crossorigin) and add &display=swap to the URL. When you host fonts yourself, use .woff2, preload the one or two fonts used above the fold, and set font-display: swap in @font-face, so text shows in a fallback font immediately instead of staying invisible.

Explain it without notes

01

Compare a plain script, async, defer and type="module".

02

Why are stylesheets render-blocking, and how does media="print" change that?

03

What is the difference between preload, prefetch and preconnect?

04

Why do font preloads need crossorigin?

Practice

01

A page loads jquery.js, then carousel.js (which uses jQuery), plus an independent chat-widget.js. Write the three script tags so nothing blocks parsing and the order is safe.

02

Write the head tags to use the Google Font Poppins (weights 400 and 600) as fast as possible.

Trade-offs

  • ↔

    Inlining critical CSS in a <style> speeds the first paint, but duplicates CSS in every page and complicates the build; a single cached stylesheet is simpler and fine for most sites.

  • ↔

    Self-hosting fonts removes the extra connection to a font CDN and gives you full control, but you handle subsetting, formats and cache headers yourself.

Done when you can

  • I can say when to use defer, async and type="module".

  • I know stylesheets are render-blocking and how media changes that.

  • I can write preload correctly, including as and crossorigin for fonts.

  • I know the difference between preload, prefetch, preconnect and dns-prefetch.

  • I can load a web font without invisible text.