Loading CSS, JavaScript and fonts
The browser does not start rendering the page until it has the styles, and an ordinary script in the head even stops reading the HTML. How to load CSS and JavaScript so that they do not slow down the first display, what preload and preconnect are for, how to keep fonts from delaying the text and how not to slow down the responses of the page.
What blocks the display of the page
- CSS in the head blocks rendering: the browser waits until it has all the styles so that it does not show the page without them first. That is why the styles should be small and on your own server.
- A script <script src="…"> without further attributes stops reading the HTML – the browser has to download and run it before it continues (the script may write into the page).
The defer and async attributes
<link rel="stylesheet" href="/css/styles.css"> <script src="/js/library.js" defer></script> <script src="/js/menu.js" defer></script> <script src="/js/analytics.js" async></script>
- defer – the script is downloaded in parallel with reading the HTML and runs when the whole HTML has been read, still before the DOMContentLoaded event. Scripts with defer run in the order in which they appear in the HTML, even when the second one is downloaded first – menu.js can use library.js.
- async – it is downloaded in parallel and runs right after downloading, regardless of the order and whether the HTML has been read. It is suitable for independent scripts such as traffic analytics.
- type="module" – modules behave like defer by themselves.
- For a script written directly into the page (without src), defer and async have no effect.
Scripts with defer in the head are better than scripts at the end of body: the browser starts downloading them right at the beginning, not only after reading the whole page.
Styles for printing and other media
<link rel="stylesheet" href="/css/styles.css"> <link rel="stylesheet" href="/css/print.css" media="print">
The browser downloads a style with a media attribute that does not apply at the moment with low priority and does not wait for it with rendering the page. Large sites also put the styles for the top part of the page directly into the head and load the rest later. For a smaller site, one smaller style file without unused rules is enough – the Coverage tab in the developer tools of Chrome shows which rules those are.
preconnect and preload
<link rel="preconnect" href="https://www.googletagmanager.com"> <link rel="preload" href="/fonts/roboto-400.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="/images/hero.webp" as="image" fetchpriority="high">
- preconnect – connects to another server in advance (DNS, connection, encryption), so a later file from there arrives sooner. It makes sense for one or two servers that the page will certainly use.
- preload – downloads a file right away, even when the page will use it only later. It is suitable for files that the browser finds late: fonts and background images given in CSS or an LCP image inserted by a script. The as attribute is required (it determines the priority and type of the request).
- Fonts are downloaded in CORS mode, even from your own server. A font preload therefore needs crossorigin, otherwise Chrome and Firefox download it twice – once for the preload and again for the CSS.
- When too much is preloaded, the files take bandwidth from each other. Preload only what is really needed right away.
Fonts and font-display
A custom font is downloaded separately. Until it arrives, the browser either does not display the text or shows it in a fallback font. The behavior is controlled by the font-display property in the @font-face rule:
@font-face {
font-family: "Roboto";
src: url("/fonts/roboto-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
body {
font-family: "Roboto", Arial, sans-serif;
}
- swap – the text is shown right away in the fallback font and the font is swapped after downloading. The text is visible soonest, but the swap may shift the text (CLS);
- optional – when the font does not arrive almost immediately, the page stays with the fallback font and the custom one is used only on the next display from the cache. Best for CLS;
- block – the text is invisible for a while (up to 3 s), fallback is between block and optional, auto leaves the choice to the browser.
The WOFF2 format is known to all current browsers; older formats are no longer needed. Load only the styles that the page really uses – each weight and italic is another file. Fonts are better hosted on your own server than loaded from Google Fonts: the connection to another server disappears and Google does not get the IP addresses of the visitors (in January 2022 a German court in Munich ruled that loading Google Fonts without consent violates the GDPR). System fonts work without any download at all.
Third-party scripts
Analytics, chat, ads, social media buttons and embedded videos add connections to other servers and often a lot of JavaScript. That then delays the responses of the page (INP). Use only what you really need, load them with async and embedded videos with loading="lazy" (see Images: formats, sizes and lazy loading). How much each script costs is shown by the Performance or Lighthouse tab in the developer tools.
Long tasks and INP
JavaScript runs in the main thread of the browser, which also handles clicks and rendering. When a script computes for longer than 50 ms (a “long task”), the page makes the click wait. Long work can be split up, letting the browser take its turn between the parts:
function pause() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((done) => setTimeout(done, 0));
}
async function processAll(items, process) {
for (let i = 0; i < items.length; i++) {
process(items[i]);
if (i % 100 === 99) await pause(); // let the browser take its turn after every 100 items
}
}
The scheduler.yield() function puts the interrupted work back at the front of the queue. It is available in Chrome since version 129 and Firefox since 142, not yet in Safari (as of October 2026). Elsewhere the example uses setTimeout with a zero delay, which also lets waiting clicks and rendering take their turn.
Related articles:
Images: formats, sizes and lazy loading
Browser cache: Cache-Control, ETag and file versions
Selecting elements on the page (querySelector)