Skip to main content

Use defer and async on non-critical scripts

Use the defer attribute on non-critical scripts to prevent them from blocking the HTML parser. Use async only for independent, third-party scripts where execution order doesn't matter.


When the browser encounters a <script src=""> tag without defer or async, it stops parsing HTML, downloads the script, executes it, and then resumes parsing. Nothing below that script tag is discovered or rendered until the script finishes.

This has two costs:

  1. Delayed rendering. The browser can't paint anything that depends on HTML below the blocking script. This delays FCP and LCP.
  2. Blocked resource discovery. The browser's preload scanner finds images, stylesheets, and other resources by scanning ahead through the HTML. A blocking script pauses that scan, so resources further down the page aren't fetched until the script completes.

Adding defer or async lets the browser continue parsing HTML while the script downloads, keeping the preload scanner working and allowing earlier paint.


Scripts with defer download in the background and execute in document order after the HTML finishes parsing, but before the DOMContentLoaded event fires. This makes defer the right choice for most theme scripts because:

  • Scripts that depend on the DOM can rely on the full document being available.
  • Scripts that depend on each other run in the order that they appear in the HTML.
  • The browser parses the entire document without interruption.
<head>
<script src="{{ 'theme.js' | asset_url }}" defer></script>
<script src="{{ 'product-form.js' | asset_url }}" defer></script>
<script src="{{ 'cart-drawer.js' | asset_url }}" defer></script>
</head>

All three scripts download in parallel while the HTML parses. They execute in order after parsing completes.

Anchor to Use ,[object Object], for independent third-party scriptsUse async for independent third-party scripts

Scripts with async download in the background and execute as soon as they finish downloading. There's no guaranteed execution order, and execution can still briefly block the main thread.

Use async for scripts that are fully independent, don't rely on the DOM, and don't depend on other scripts. Analytics and tracking pixels are the most common examples.

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

If a third-party script does depend on the DOM or on other scripts, then use defer instead.

Anchor to Know what not to deferKnow what not to defer

Some scripts must run synchronously by design:

  • A/B testing anti-flicker snippets. These hide page content until a variant is selected. Deferring them causes a flash of the original content before the variant applies. If you aren't actively running a test, remove the snippet entirely.
  • Scripts that modify content in the initial viewport before render. If a script rewrites DOM that's visible without scrolling, then deferring it causes a visible flash or layout shift.

Anchor to Remove scripts that aren't neededRemove scripts that aren't needed

Before deciding whether to defer a script, ask whether it's needed at all. Every script, even deferred, consumes bandwidth and main-thread time during execution. Removing unnecessary scripts is always better than deferring them.

See Audit and remove costly third-party scripts for guidance on evaluating third-party script cost.

For scripts tied to specific user interactions, such as chat widgets, video players, and dialogs, consider loading them only when the user interacts with the feature. See Load JavaScript on user interaction for that pattern.


Anchor to Example 1: Theme script loading in ,[object Object]Example 1: Theme script loading in theme.liquid

Before: Synchronous scripts in <head> block HTML parsing.

<head>
<script src="{{ 'vendor.js' | asset_url }}"></script>
<script src="{{ 'theme.js' | asset_url }}"></script>
<script src="{{ 'product-form.js' | asset_url }}"></script>
</head>

After: Deferred scripts download in parallel and execute in order after parsing.

<head>
<script src="{{ 'vendor.js' | asset_url }}" defer></script>
<script src="{{ 'theme.js' | asset_url }}" defer></script>
<script src="{{ 'product-form.js' | asset_url }}" defer></script>
</head>

Anchor to Example 2: Third-party analyticsExample 2: Third-party analytics

Before: A synchronous analytics script in <head> blocks page rendering.

<head>
<script src="https://analytics.example.com/tracker.js"></script>
</head>

After: async lets the browser continue parsing while the script downloads. Because the analytics script is independent and doesn't rely on execution order, async is appropriate here.

<head>
<script src="https://analytics.example.com/tracker.js" async></script>
</head>

Anchor to Example 3: Conditional script loadingExample 3: Conditional script loading

Not every page needs every script. Use the Liquid template object to load scripts only on pages that require them. This reduces both download and execution cost on pages where the script does nothing.

<head>
{%- comment -%} Core theme scripts load on every page {%- endcomment -%}
<script src="{{ 'theme.js' | asset_url }}" defer></script>

{%- comment -%} Load product form logic only on product pages {%- endcomment -%}
{% if template.name == 'product' %}
<script src="{{ 'product-form.js' | asset_url }}" defer></script>
{% endif %}

{%- comment -%} Load the cart drawer only when the cart type is drawer {%- endcomment -%}
{% if settings.cart_type == 'drawer' %}
<script src="{{ 'cart-drawer.js' | asset_url }}" defer></script>
{% endif %}

{%- comment -%} Load search predictions only on pages with a search form {%- endcomment -%}
{% if template.name == 'search' or settings.predictive_search_enabled %}
<script src="{{ 'search.js' | asset_url }}" defer></script>
{% endif %}
</head>

  • Network panel waterfall: Open Chrome DevTools, go to the Network panel, and reload the page. Synchronous scripts show a solid bar that blocks subsequent resource loading. Deferred scripts download in parallel with other resources.
  • Performance panel, Insights tab: Record a page load and look for Render blocking requests in the summary. Any script without defer or async appears here.
  • Before and after comparison: Record a Performance panel trace with synchronous scripts, add defer, then record again. Compare FCP and LCP between the two recordings.


Was this page helpful?