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.
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
Parser blocks on download + execute
Parallel download, execute after parse
Parallel download, executes immediately (out of order)
Resource hints
Requires as= attribute. Fonts need crossorigin.
Browser uses idle time. Good for paginated content.
Use for critical external origins (fonts, API). Max 2-3.
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.
Remember this
- Plain
<script>is parser-blocking. Place scripts before</body>, or usedeferorasync. 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. Theasattribute is required.
Done with this concept?
Mark it complete to track your progress. No login needed.