Deferring non-critical CSS/JS with async or deferfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Think of render-blocking resources as a single-file checkout line: even if nine of ten items scan instantly, the whole line stops the moment one item needs a price check. A browser painting your page works the same way — if it hits a <script> or <link rel="stylesheet"> tag that must fully download and execute before rendering can continue, everything after it on the page waits, even content that had nothing to do with that resource.

What you'll learn in this lesson

  • Why the browser blocks rendering on certain <script> and <link> tags by default
  • How to mark a library's JavaScript as safe to load asynchronously
  • The real difference between async and defer, and when each is correct
  • Why some scripts genuinely can't be deferred, and how to spot them

Why the browser blocks in the first place

By default, when a browser's HTML parser hits a <script> tag, it stops parsing the rest of the document, downloads the script (if external), executes it, and only then resumes. This is intentional: a script might use document.write() to inject content into the page, so the browser can't safely skip ahead without risking rendering the page in the wrong order. CSS is similarly blocking for a different reason — the browser won't paint anything until it knows the final styles, to avoid a visible "flash of unstyled content."

Marking a library's JS as non-blocking

mymodule.chart-widget:
  js:
    js/chart-widget.js:
      attributes:
        defer: true
  css:
    theme:
      css/chart-widget.css: {}
  dependencies:
    - core/once

Drupal's *.libraries.yml format lets you attach raw HTML attributes to a JS asset via the attributes key — exactly like the real library definitions you've already seen in this course (e.g. js_example.libraries.yml). Adding defer: true here is all it takes; Drupal renders that attribute directly onto the generated <script> tag.

async vs. defer — genuinely different, not interchangeable

Both attributes tell the browser "don't block parsing to fetch this script" — but they diverge on when the script actually executes:

  • defer: the script downloads in the background and executes only after the HTML document has finished parsing, in the order the <script> tags appear. Safe default for most application logic that needs the DOM to already exist.
  • async: the script downloads in the background and executes the moment it finishes downloading — possibly before parsing is done, and in whatever order each script happens to finish, not document order. Appropriate for fully independent scripts (analytics, ads) that don't touch the DOM and don't depend on any other script.
What you can't defer: a script that uses document.write(), or one that another inline script depends on running first, will break if you mark it async/defer carelessly. Drupal's own core JavaScript (like core/drupal, which most of your own libraries depend on) is intentionally left blocking for exactly this reason — deferring it would risk other scripts running before Drupal's own JS API exists.

CSS: preload instead of defer

CSS doesn't have an async/defer equivalent, since a page genuinely shouldn't paint with wrong styles. For CSS that's only needed below the fold or for a rarely-used feature, the real technique is <link rel="preload"> combined with swapping the rel attribute via a small inline script once loaded — Drupal doesn't have a built-in library-definition key for this pattern, so it's typically hand-rolled in a theme's page template for the specific stylesheets that genuinely don't affect above-the-fold content.

Quick check: you have two scripts — one that reads drupalSettings and manipulates the DOM, and a completely separate analytics snippet with no dependencies on anything else on the page. Which one is the safer candidate for async? If you said the analytics snippet, you've got it — async gives no execution-order guarantee, which is exactly wrong for a script that depends on the DOM or another script having already run.

Key takeaways

  • Scripts and stylesheets block rendering by default because the browser can't safely guess what they'll do to the page.
  • Add attributes: { defer: true } (or async: true) under a JS asset in *.libraries.yml to opt it out of blocking.
  • defer preserves document order and waits for parsing to finish; async guarantees neither — use it only for scripts with zero dependencies.
  • Never defer/async a script that depends on another script or on document.write() — check dependencies first.
  • CSS has no defer equivalent; use rel="preload" with a JS-driven rel swap for genuinely non-critical stylesheets.

Coming up next

You've now covered the full frontend-optimization toolkit: aggregation, minification, lazy images, responsive styles, font loading, and now render-blocking resources. Next, we shift focus to how your module declares and manages its assets in the first place — starting with what makes a library definition genuinely efficient.