RevealTheme logo
Back to Blog

How to Defer JavaScript Without Breaking WordPress

How to Defer JavaScript Without Breaking WordPress
The RevealTheme Team

By

·

Deferring JavaScript is one of the highest-leverage speed wins available to a WordPress site: render-blocking scripts are usually the single biggest reason a page feels slow before anything is even visible. But it is also the optimization most likely to break a site outright — a deferred script that runs out of order can leave you with a dead slider, a frozen menu, or a console full of $ is not defined errors. This guide explains exactly why deferring breaks WordPress and how to do it safely, whether you prefer code or a plugin.

What "defer" actually does (and why it's not "async")

A normal <script> tag is render-blocking: the browser stops parsing HTML, downloads the script, executes it, and only then continues. Both defer and async let the download happen in parallel without blocking the parser, but they execute very differently — and the difference is the entire ballgame for WordPress.

  • defer — the script downloads in parallel, then executes after the HTML is fully parsed, and crucially, deferred scripts run in the order they appear in the document.
  • async — the script downloads in parallel and executes the instant it finishes downloading. Execution order is not guaranteed, so a script can run before its dependency has loaded.

For WordPress you almost always want defer, not async. WordPress is built on chains of dependent scripts — jQuery loads first, then jQuery Migrate, then a plugin's script that calls into jQuery, then your theme's script. async shatters that order and your page breaks at random. defer preserves it. Treat async as a tool for genuinely standalone scripts only.

Why deferring breaks WordPress: the jQuery trap

The number-one cause of "I deferred my JS and the site broke" is jQuery. A huge share of plugins and themes still register inline scripts that call jQuery(...) or $(...) directly in the page body. Those inline snippets are not deferred — they run the moment the parser reaches them. If you have deferred jQuery itself, the inline call fires before jQuery exists, and you get:

  • Uncaught ReferenceError: jQuery is not defined
  • Uncaught ReferenceError: $ is not defined

Everything downstream of that error stops executing — which is why a single mis-deferred script can take out an entire page's interactivity. The lesson: deferring is only safe when the whole dependency chain is handled consistently, and inline scripts that assume synchronous jQuery are accounted for. This is exactly the problem WordPress core solved in 2023.

The modern, safe way: WordPress 6.3+ script strategies

Since WordPress 6.3 (August 2023), wp_enqueue_script() and wp_register_script() accept an arguments array with a strategy key. This is the correct 2026 approach and most tutorials still don't mention it:

add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'my-theme-main',
        get_theme_file_uri( '/assets/js/main.js' ),
        array( 'jquery' ),       // dependencies
        '1.0.0',
        array(
            'strategy'  => 'defer',  // or 'async'
            'in_footer' => true,
        )
    );
} );

The reason this is safer than hand-editing tags is that WordPress's strategy system is dependency-aware. If you ask to defer a script that another, non-deferred script depends on, WordPress will quietly decline to defer it rather than break the chain. It evaluates the whole graph and only applies the strategy where it is provably safe. You get the speed benefit without the order-of-execution roulette.

If a script you don't own isn't being deferred, you can re-register it with a strategy by deregistering and re-adding it, but check the dependency tree first — fighting core's safety logic usually means you'll reintroduce the very breakage it's protecting you from.

The older filter approach (and why to retire it)

Before 6.3, the common technique was to hook script_loader_tag and string-manipulate the HTML to inject a defer attribute:

add_filter( 'script_loader_tag', function ( $tag, $handle ) {
    $defer = array( 'my-theme-main', 'some-plugin-script' );
    if ( in_array( $handle, $defer, true ) ) {
        return str_replace( ' src', ' defer src', $tag );
    }
    return $tag;
}, 10, 2 );

This still works, but it is "dumb" — it has no idea what depends on what, so the responsibility for not breaking the dependency graph falls entirely on you. On WordPress 6.3 and later, prefer the strategy argument. Reserve the filter for the rare script you can't reach through the enqueue API.

Doing it with a plugin instead

If you'd rather not touch code, several plugins handle deferral with a checkbox — but they differ in how aggressive and how safe they are:

  • WP Rocket — "Load JavaScript deferred" applies defer site-wide with a built-in exclusion list and an editable one. Its separate, far more aggressive "Delay JavaScript execution" is a different technique (see below).
  • Perfmatters — a lightweight toggle for "Defer JavaScript" plus "Delay JavaScript," with a clean per-script exclusion interface. Popular for surgical control without a full caching suite.
  • FlyingPress — defers and can delay JS, and is known for sensible defaults that avoid breaking common page builders.
  • LiteSpeed Cache — free and powerful if you're on LiteSpeed/OpenLiteSpeed hosting; includes "Load JS Deferred" under its Page Optimization tab.
  • Autoptimize — free, with an "aggregate and defer" option, though its JS handling is older and more likely to need exclusions on modern stacks.
  • SiteGround Optimizer — if you're hosted on SiteGround, its "Defer Render-blocking JS" toggle is well-integrated.

Whichever you choose, the workflow is the same: enable deferral globally, then build an exclusion list of the handful of scripts that break.

The exclusion-list workflow

"Defer broadly, exclude what breaks" is the practical methodology behind doing this without breaking anything. Common culprits that usually need excluding from deferral:

  • Sliders and carousels (Slick, Swiper-driven sliders)
  • Lightboxes and galleries
  • Sticky headers and mega menus
  • Cookie-consent banners (which legally must appear instantly)
  • Google reCAPTCHA and form scripts
  • Above-the-fold interactive widgets

Work through the site after enabling deferral, watch the browser console for errors, and add each broken script's handle or filename to the exclusion list until everything works.

Defer vs. "delay until interaction"

Don't confuse deferral with delaying JavaScript until user interaction — a distinct, more aggressive technique offered by WP Rocket, Perfmatters, and FlyingPress. Delay holds scripts back entirely until the visitor scrolls, moves the mouse, or taps, which is ideal for heavy third-party tags: Google Tag Manager, analytics, chat widgets, and ad scripts. It produces dramatic lab-score gains but is more prone to breaking above-the-fold interactivity, so it needs careful exclusions. Defer is the safe baseline; delay is the power tool you reach for once defer is stable.

How to measure whether it worked

Deferring JavaScript mainly improves First Contentful Paint and lab metrics like Total Blocking Time, and it helps real-world Interaction to Next Paint (INP) — the Core Web Vital that replaced FID in March 2024. Aim for INP under 200ms. It can also help Largest Contentful Paint (target under 2.5s) when a render-blocking script was holding up your main content, though if your LCP element is loaded by JavaScript, aggressive deferral can briefly hurt it instead.

Test before and after in PageSpeed Insights (which reports both lab and field data) and confirm with a real run-through of the site in an incognito window. If your numbers improved and nothing in the console is red, you've deferred JavaScript without breaking WordPress — which is the whole point.