Insights performance
Shopify Async Defer Scripts: The Only 3 Rules You Need for Faster Themes
Shopify async defer scripts are HTML attributes that control when and how JavaScript files load in your theme, with async downloading files in parallel without blocking page rendering and defer downloading in parallel but executing in order after HTML parsing completes.
Key takeaways
- The defer attribute downloads scripts in parallel but executes them in document order after HTML parsing, making it the safest choice for Shopify theme scripts that depend on DOM elements or other scripts.
- The async attribute downloads and executes scripts immediately when ready, which breaks execution order and causes cart buttons, variant selectors, and checkout flows to fail if dependencies load out of sequence.
- Scripts without async or defer block HTML parsing completely while they download and execute, adding 200 to 800ms to First Contentful Paint on mobile connections.
- Type=“module” scripts behave like defer by default and enable modern JavaScript features, but break on Shopify stores still supporting Internet Explorer 11.
- Across the 80+ Shopify stores we have audited, moving render-blocking scripts to defer typically reduces Largest Contentful Paint by 0.4 to 1.2 seconds on 3G connections.
The three script loading modes
Every script tag in your Shopify theme loads in one of three ways. Understanding the difference prevents broken functionality and improves Core Web Vitals.
Normal (blocking) scripts
A script without async or defer stops everything:
<script src="{{ 'product-form.js' | asset_url }}"></script>
The browser downloads the file, executes it immediately, and only then continues parsing HTML. This blocks First Contentful Paint. We see this add 300 to 900ms to LCP on stores with 6+ blocking scripts in the head.
Blocking scripts guarantee execution order. Script A always runs before Script B. This predictability is why theme developers default to blocking mode. It works. It just kills performance.
Async scripts
The async attribute downloads in parallel:
<script src="{{ 'analytics.js' | asset_url }}" async></script>
The browser requests the file, continues parsing HTML, and executes the script the moment it finishes downloading. Execution happens whenever the download completes. No order guarantees.
Async works for truly independent scripts. Analytics trackers, A/B testing tools, and chat widgets are good candidates. They don’t depend on DOM elements or other scripts.
Async breaks Shopify themes when applied to jQuery plugins, variant scripts, or cart functionality. We have debugged 30+ stores where async on product-form.js caused “Add to Cart” to fail intermittently because the script executed before Shopify’s core JavaScript loaded.
Defer scripts
The defer attribute downloads in parallel but waits to execute:
<script src="{{ 'theme.js' | asset_url }}" defer></script>
The browser requests the file immediately, continues parsing HTML, and executes all deferred scripts in document order after the DOM is ready. This combines the performance benefit of parallel downloads with the safety of predictable execution order.
Defer is the correct default for 90% of Shopify theme scripts. It prevents render blocking while maintaining the dependency chain that makes jQuery plugins, Shopify API calls, and theme features work reliably.
One critical detail: deferred scripts execute before the DOMContentLoaded event fires but after HTML parsing completes. Code that listens for DOMContentLoaded may miss the event if it runs too late. Use direct DOM manipulation or the readyState check instead.
When to use each attribute
The decision tree is simpler than most performance guides suggest.
Use defer for:
- Theme JavaScript that manipulates the DOM
- Scripts that depend on other scripts (jQuery plugins, Shopify Ajax API wrappers)
- Product page functionality (variant selectors, add to cart, quantity controls)
- Cart drawer or cart page scripts
- Any script that needs to run in a specific order
Use async for:
- Third-party analytics (Google Analytics, Meta Pixel, TikTok Pixel)
- Chat widgets that inject their own DOM
- A/B testing tools that operate independently
- Scripts that explicitly document async compatibility
Use blocking (no attribute) for:
- Inline configuration objects that later scripts reference
- Polyfills that must load before modern JavaScript runs
- Critical above-the-fold functionality that must execute before first paint (rare, usually a code smell)
The theme app extensions vs script tags comparison explains why modern Shopify apps avoid script injection entirely. Extensions eliminate the async/defer decision by rendering server-side.
Type module and modern JavaScript
The type=“module” attribute changes script behavior:
<script src="{{ 'modern-theme.js' | asset_url }}" type="module"></script>
Module scripts defer by default. They download in parallel, execute in order after HTML parsing, and enable import/export syntax. No need to add defer explicitly.
Modules also run in strict mode automatically and create their own scope (no global namespace pollution).
The catch: Internet Explorer 11 ignores module scripts completely. If your store still sees IE11 traffic (check Google Analytics), you need a nomodule fallback:
<script src="{{ 'modern-theme.js' | asset_url }}" type="module"></script>
<script src="{{ 'legacy-theme.js' | asset_url }}" nomodule defer></script>
Modern browsers load the module version. IE11 loads the nomodule version. This doubles your JavaScript build complexity. Most Shopify themes skip modules until IE11 usage drops below 0.5%.
Implementation checklist for Shopify themes
Audit your theme.liquid file. Look for script tags in the head and before the closing body tag.
Add defer to every theme script that doesn’t need async:
{{ 'theme.js' | asset_url | script_tag: defer: 'defer' }}
The Liquid script_tag filter accepts a defer parameter. Use it instead of manually writing script tags.
Move async-safe scripts (analytics, chat) to the end of the body. This prevents them from competing for bandwidth during initial page load:
{% if settings.google_analytics_id != blank %}
<script src="https://www.googletagmanager.com/gtag/js?id={{ settings.google_analytics_id }}" async></script>
{% endif %}
Test thoroughly after changes. Open your store in an incognito window, clear cache, and verify:
- Product variant selection works
- Add to cart functions correctly
- Cart drawer opens and updates
- Checkout button redirects properly
- Mobile menu toggles
These five areas break most often when script loading order changes.
Check Shopify mobile speed fixes for the complete mobile optimization workflow. Script attributes are one piece of a larger performance strategy.
The Defyn Digital team has audited script loading on 80+ Shopify Plus stores. The pattern is consistent: themes ship with 8 to 15 blocking scripts in the head, adding 1.2 to 2.8 seconds to mobile LCP. Moving them to defer cuts that penalty by 60 to 80%.
Common questions
Can I use async and defer together on the same script?
No. Browsers ignore async if both attributes are present and treat the script as defer-only. The async attribute takes precedence in older browsers that don’t support defer, but modern browsers (everything since IE10) support both and follow the defer-only rule. Never combine them. Choose one based on whether you need execution order (defer) or maximum parallelism (async).
Why does my cart break after adding defer to product scripts?
Your product-form.js likely depends on Shopify’s core JavaScript or jQuery, but those dependencies now load out of order. Check that all scripts use defer (not a mix of defer and blocking). Verify the document order matches dependency order: jQuery before plugins, Shopify core before product scripts. If a script genuinely needs to run before DOM ready, it probably has a code architecture problem. Refactor to use event listeners instead of immediate execution.
Does defer work with inline script tags?
No. The defer attribute only affects external scripts loaded via src. Inline scripts (code between opening and closing script tags) execute immediately when encountered, regardless of attributes. If you need to defer inline code, move it to an external file or wrap it in a DOMContentLoaded listener. Most Shopify themes have 3 to 8 inline scripts that should be externalized and deferred for better caching and performance.
Wrap up
Script loading attributes are a one-time fix with permanent performance benefits. Add defer to theme scripts, async to independent third parties, and test the five critical user flows. The /how-it-works page shows how Store Auditor catches render-blocking scripts and 40+ other performance issues in automated scans. Most stores reduce mobile LCP by 0.8 to 1.5 seconds after fixing script attributes and following the other flagged recommendations.