Defer/Async JavaScript for Better Performance

September 27, 2025
Defer/Async JavaScript for Better Performance

Large or render-blocking JavaScript can slow down your WordPress site. Two safe, high-impact techniques are defer and async. This guide explains when to use each and how to implement them properly in WordPress (including modern strategy support and backward-compatible methods).

Defer vs Async: What’s the Difference?

  • defer: The browser downloads the script in parallel but executes it after HTML parsing, in document order. Best when your scripts depend on each other or on DOM structure.
  • async: The browser downloads the script in parallel and executes it as soon as it finishes, out of order. Best for independent, third-party scripts (analytics, ads, widgets) that don’t affect layout logic.

Rule of thumb: If the script has dependencies or needs the DOM ready, use defer. If it’s fire-and-forget, use async.

Baseline: Load Scripts in the Footer

Before tweaking attributes, make sure non-critical scripts load in the footer to reduce blocking in the head:

<?php
add_action( 'wp_enqueue_scripts', function () {
    // Example: load theme.js in footer (the 5th param or 'in_footer' => true)
    wp_enqueue_script(
        'theme-scripts',
        get_template_directory_uri() . '/assets/js/theme.js',
        array(),         // dependencies
        '1.0.0',
        true             // in_footer
    );
} );

Method 1 (WP 6.3+): Use the strategy Argument

WordPress 6.3 introduced a first-class way to set loading strategy per script. Prefer this API if your site runs on WP 6.3 or later.

<?php
add_action( 'wp_enqueue_scripts', function () {
    // Defer a dependent bundle (keeps execution order)
    wp_enqueue_script(
        'app-bundle',
        get_template_directory_uri() . '/assets/js/app.bundle.js',
        array( 'wp-element' ), // example dependency
        '1.2.3',
        array(
            'in_footer' => true,
            'strategy'  => 'defer', // 'defer' or 'async'
        )
    );

    // Async a third-party script (no dependencies)
    wp_enqueue_script(
        'awesome-analytics',
        'https://cdn.example.com/awesome-analytics.min.js',
        array(),
        null,
        array(
            'in_footer'   => true,
            'strategy'    => 'async',
            'crossorigin' => 'anonymous', // optional if required
        )
    );
} );

Method 2 (Backward-Compatible): wp_script_add_data()

For WordPress versions before 6.3, add attributes via wp_script_add_data(). Use this when you can’t rely on strategy.

<?php
add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'theme-scripts',
        get_template_directory_uri() . '/assets/js/theme.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );

    // Defer keeps order with dependencies
    wp_script_add_data( 'theme-scripts', 'defer', true );

    // Independent third-party: async
    wp_enqueue_script(
        'maps-api',
        'https://example.com/maps.js',
        array(),
        null,
        true
    );
    wp_script_add_data( 'maps-api', 'async', true );
} );

Method 3 (Bulk): Filter to Defer Most Scripts

If you want to defer everything except a short allow-list, use script_loader_tag. Be careful: over-eager deferring can break inline code that expects libraries immediately.

<?php
add_filter( 'script_loader_tag', function ( $tag, $handle, $src ) {
    $exclude = array(
        'jquery',             // often needed early by legacy themes
        'wp-polyfill',        // core polyfills
        'contact-form-7',     // example plugin handle
    );

    if ( in_array( $handle, $exclude, true ) ) {
        return $tag; // do not alter
    }

    // Add defer if not already async/defer and it's a normal script
    if ( false === strpos( $tag, 'defer' ) && false === strpos( $tag, 'async' ) ) {
        $tag = str_replace( '<script ', '<script defer ', $tag );
    }
    return $tag;
}, 10, 3 );

Inline Scripts That Depend on a Handle

When you add inline code that relies on a handle (e.g., your bundle exposes a global), attach inline code after that handle so it runs in the right order—even with defer:

<?php
add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'app',
        get_template_directory_uri() . '/assets/js/app.js',
        array(),
        '1.0',
        true
    );
    wp_script_add_data( 'app', 'defer', true );

    $inline = 'window.App && window.App.init && window.App.init();';
    wp_add_inline_script( 'app', $inline, 'after' );
} );

ES Modules: type="module" Is Deferred by Default

Module scripts are parsed as modules and are effectively deferred by default. You can combine this with a nomodule fallback for legacy browsers if needed.

<?php
add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'module-app',
        get_template_directory_uri() . '/assets/js/app.module.js',
        array(),
        '1.0.0',
        array(
            'in_footer' => true,
            'type'      => 'module', // WP will output type="module"
        )
    );

    // Optional: nomodule fallback for old browsers
    wp_enqueue_script(
        'nomodule-app',
        get_template_directory_uri() . '/assets/js/app.legacy.js',
        array(),
        '1.0.0',
        true
    );
    wp_script_add_data( 'nomodule-app', 'nomodule', true );
} );

Preload + Defer for Critical Bundles

If a deferred bundle is critical for interaction, you can hint the network earlier using preload, then execute with defer:

<?php
add_action( 'wp_head', function () {
    $src = esc_url( get_template_directory_uri() . '/assets/js/app.bundle.js' );
    echo '<link rel="preload" as="script" href="' . $src . '">';
}, 1 );

add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'app-bundle',
        get_template_directory_uri() . '/assets/js/app.bundle.js',
        array(),
        '1.0.0',
        true
    );
    wp_script_add_data( 'app-bundle', 'defer', true );
} );

When Not to Use Async/Defer

  • Inline scripts in the head that expect a library (e.g., jQuery) to be available immediately.
  • A/B testing snippets that must run before rendering to avoid layout flicker.
  • Scripts that intentionally block rendering for UX reasons (rare).

Compatibility Tips

  • Dependencies: Use defer when scripts depend on each other; avoid async in dependency chains.
  • jQuery-heavy themes: Keep jQuery in the head or defer it together with any code that uses it (attach inline with wp_add_inline_script).
  • Third-party: Prefer async. Add crossorigin and referrerpolicy if the provider requires it.
  • Measure: Verify improvements with Lighthouse/PageSpeed Insights. Watch First Contentful Paint, Largest Contentful Paint, and Total Blocking Time.

Quick Checklist

  • Move non-critical scripts to the footer.
  • Use strategy: 'defer' (WP 6.3+) for theme/app bundles.
  • Use strategy: 'async' for independent third-party scripts.
  • Attach inline code with wp_add_inline_script( 'handle', $code, 'after' ).
  • Optionally preload critical deferred bundles.
  • Test on real devices and roll back handles that break.

Conclusion

Smart use of defer and async can significantly reduce render-blocking and improve Core Web Vitals. On modern WordPress, prefer the strategy argument for clarity and safety; otherwise, use wp_script_add_data(). Start with defer for your bundles and async for third-party scripts, then validate the impact with performance audits.

Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.