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
importandexport. Loaded withtype="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.
<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.
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.
<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.
<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.
<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.
<link rel="prefetch" href="/welcome">Try it yourself
- 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
Make the module async
Add
asyncto 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
Turn the print sheet on
In the second example, change
media="print"tomedia="screen". Predict the heading colour first. Which stylesheet wins now, and why?
Code & diagrams
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
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. DOMContentLoadedBoth 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
Console
Nothing logged yet.Expected console output
Screen colour: rgb(220, 20, 60)
Print sheet applies now: false<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.
Break #2
Preloading a font without crossorigin
Write <link rel="preload" href="/fonts/inter.woff2" as="font"> without crossorigin.
Break #3
Preloading things you don't use
Preload five images and three fonts "just in case".
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/imagesizespair, 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 oldrel="prerender". Other browsers ignore it, so it is safe progressive enhancement. - ▸
A
103 Early HintsHTTP response lets the server sendLink: <...>; rel=preloadheaders before the page itself is ready, so preloads start while the server is still building the HTML (see/system-designfor HTTP and caching).
Remember this
- 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. Amediaattribute 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
A plain
<script src="app.js">is parser-blocking: the parser stops, waits for the download, runs the script, then continues.deferdownloads the script in parallel with parsing and runs it after the document has been parsed, in the order the scripts appear, just before theDOMContentLoadedevent (the moment the page's HTML is fully parsed). Usedeferfor your own app code. - 3
asyncalso 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. Bothdeferandasyncwork only on scripts with asrc; on an inline script they do nothing. - 4
<script type="module">loads a JavaScript module (a file that canimportandexportcode). 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. Addasyncto 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
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;asis required (style,script,font,image,fetch) and fonts also needcrossorigin.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
Fonts are a common slowdown: the browser only discovers a font after it has the CSS that uses it. For Google Fonts,
preconnecttohttps://fonts.googleapis.comandhttps://fonts.gstatic.com(the second withcrossorigin) and add&display=swapto the URL. When you host fonts yourself, use.woff2, preload the one or two fonts used above the fold, and setfont-display: swapin@font-face, so text shows in a fallback font immediately instead of staying invisible.
Explain it without notes
Compare a plain script, async, defer and type="module".
Why are stylesheets render-blocking, and how does media="print" change that?
What is the difference between preload, prefetch and preconnect?
Why do font preloads need crossorigin?
Practice
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.
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,asyncandtype="module".I know stylesheets are render-blocking and how
mediachanges that.I can write
preloadcorrectly, includingasandcrossoriginfor fonts.I know the difference between preload, prefetch, preconnect and dns-prefetch.
I can load a web font without invisible text.