⌨️ Web development

Loading JavaScript and CSS: defer, async and blocking resources

Why scripts and CSS can block a page and how to use defer, async, preload and preconnect so visitors see your content sooner on every page load.

Two pages of the same weight can appear on screen at very different times. The difference often lies not in how many kilobytes are downloaded, but in when and in what order the browser receives and processes them. Understanding how the browser builds a page lets you move a few lines in the HTML and gain seconds of perceived speed.

How the browser builds the page

In simplified terms, the browser:

  1. downloads the HTML and starts parsing it from top to bottom, building the DOM;
  2. when it meets a stylesheet, it downloads it and builds the CSSOM (the style model);
  3. when it meets a script, it downloads and executes it;
  4. combines DOM and CSSOM, calculates the layout and paints the pixels.

Two kinds of resources can halt this process: CSS and synchronous scripts.

CSS blocks rendering

The browser does not paint the page until the stylesheets in the head have loaded. This is intentional: otherwise users would briefly see the unstyled page and then a sudden jump. The consequence is that a large or slow CSS file delays the appearance of all the content.

What you can do:

  • reduce the initial CSS: remove unused rules and split styles by section or page type;
  • use media attributes: a stylesheet with media="print" does not block screen rendering;
  • consider critical CSS: put the few rules needed for the above-the-fold area directly into the HTML and load the rest afterwards. It is an effective technique but complex to maintain, to be reserved for cases where it is really needed.
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/print.css" media="print">

Synchronous scripts block parsing

A classic script with no attributes stops HTML parsing until it has been downloaded and executed:

<script src="/js/app.js"></script>

The historical reason is that the script might modify the document with document.write, so the browser cannot carry on. If the script is in the head and is heavy, the user stares at a blank page.

defer

<script src="/js/app.js" defer></script>

With defer the script is downloaded in parallel with HTML parsing and executed only at the end, before the DOMContentLoaded event. Deferred scripts run in the order they appear. It is the right choice for almost all application scripts, which need the complete DOM and often depend on one another.

async

<script src="https://analytics-example.com/tag.js" async></script>

With async the script is downloaded in parallel and executed as soon as it is ready, interrupting parsing at that moment. Execution order is not guaranteed. It suits independent scripts that depend on nothing else and that nothing depends on, such as some measurement tools.

Summary

Attribute Download Execution Order
none blocks parsing immediately yes
defer in parallel after HTML parsing yes
async in parallel as soon as downloaded no
type="module" in parallel like defer yes

Module scripts are deferred by default; you can also add async to a module if you want it to run as soon as it is ready.

Priority hints: preload, preconnect and fetchpriority

The browser discovers resources as it parses HTML and CSS. Some important resources, however, are discovered late: a font referenced inside a CSS file, a background image, a script loaded by another script. Resource hints let you bring them forward.

preload

It asks for a resource that will definitely be needed to be downloaded immediately:

<link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin>

The as attribute is required for the browser to assign the correct priority. Fonts need crossorigin even when the file is on the same domain. Do not overdo it: everything you preload competes for bandwidth with everything else, and preloading resources that are then not used produces a console warning and waste.

preconnect

It opens the connection early to another domain you will load resources from shortly, saving the time spent on name resolution and secure connection negotiation:

<link rel="preconnect" href="https://cdn.example.com">

Limit it to one or two truly critical domains; for the others there is the lighter dns-prefetch.

fetchpriority

It indicates a resource's relative priority compared with others of the same type:

<img src="/img/cover.webp" fetchpriority="high" alt="...">
<script src="/js/chat-widget.js" async fetchpriority="low"></script>

Third-party scripts

Chat widgets, social buttons, maps, advertising and tracking tools are often the main cause of slow pages, and they are outside your control. Some strategies:

  • load only what you really need and review the list periodically;
  • always use async or defer;
  • for heavy components such as videos or maps, show a static preview and load the real component only on click;
  • measure the impact with the Performance panel in the developer tools, temporarily blocking a domain to see how much changes.

How to spot blocking resources

Lighthouse flags render-blocking resources with a dedicated audit. In the Network panel you can watch the request waterfall: a long chain in which each resource starts only when the previous one finishes indicates late discovery, which can be fixed with preload or by moving the references into the HTML.

In short

  • CSS in the head blocks rendering: keep it small and use media for non-essential styles.
  • Scripts without attributes block HTML parsing: use defer for application scripts.
  • async suits only independent scripts, because order is not guaranteed.
  • preload, preconnect and fetchpriority bring critical resources forward, if used sparingly.
  • Third-party scripts should be cut down, loaded asynchronously or only on demand.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. A computer science graduate, he combines rigorous technical analysis with attention to the people who use technology.

Read also