Skip to content

How to fix render-blocking CSS and JavaScript in WordPress

A render-blocking file is a stylesheet or script the browser must fetch and process before it draws the page. Find the files in the audit, defer the scripts that can wait, stop loading what a page does not use, and check the page after every change.

By
WP Ministry
Updated
Tested on
WordPress 7.1.3, PHP 8.3.35

In short

  • The audit names each blocking file. The file's address shows which plugin or theme sent it.
  • A deferred script waits until the page has been read and keeps its place in the order. An async script runs whenever it arrives.
  • Pages break when code runs before the library it calls. jQuery and the inline code that depends on it are the usual case.
  • Since WordPress 6.3 a script can be registered with a loading strategy. WordPress keeps it blocking when deferring it would upset the order.
  • Critical CSS written by hand goes out of date. Leave it to a plugin, or leave the main stylesheet alone.
  • Clearing the audit does not by itself make a page fast. The score is worked out from metrics, not from this list.

A render-blocking resource is a stylesheet or a script that the browser has to download and process before it draws the page. Until that is done, the visitor looks at a blank screen. PageSpeed Insights and Lighthouse list these files under "Render-blocking requests". Before Lighthouse 13 the list was an audit called "Eliminate render-blocking resources", and older reports and guides still use that name.

The fix has three parts. Let the scripts that can wait load without holding up the page. Stop loading files a page does not use. Leave alone what the first view needs. Any of these changes can break a menu, a form or a slider, so make one at a time on a copy of the site and look at the result.

This page covers that one audit. For the whole job, see how to speed up WordPress. For the figures Google judges a page by, see how to improve WordPress Core Web Vitals.

What counts as render-blocking

The browser reads a page's HTML from the top. Two kinds of file hold it up.

  • Stylesheets. CSS blocks rendering by default. The browser draws nothing more until it has downloaded and processed the stylesheet, because a page drawn without its styles would look broken and then change.
  • Scripts. A script loaded the ordinary way stops the browser reading the HTML until the script has been fetched and run, because the script may change the page. In the head of the page, that holds up everything below it.

Lighthouse flags a script when its tag is in the head and has neither a defer nor an async attribute. It flags a stylesheet link unless the link is disabled or carries a media attribute that does not match the device. media="all" counts as blocking. So a script printed at the end of the page, a deferred script and a print stylesheet are not flagged.

Find the files that block

  1. Step 1: Run the page through PageSpeed Insights

    Go to pagespeed.web.dev, enter the page's address and open the audit. It lists the address of every file that blocked the first paint. Test one page of each kind that matters, because each template loads its own files.

  2. Step 2: Read each address

    The folder tells you where a file comes from. An address under /wp-content/plugins/ and a folder name belongs to that plugin. One under /wp-content/themes/ belongs to the theme. One under /wp-includes/ is part of WordPress. An address on another domain is a service the site loads.

  3. Step 3: Find the file's handle

    WordPress knows each script and stylesheet by a name called a handle, and the code further down needs it. View the page's source and find the tag. A script tag's id is the handle followed by -js, and a stylesheet link's is the handle followed by -css. A tag with id="example-slider-js" has the handle example-slider.

  4. Step 4: See how much of each file the page uses

    In Chrome's developer tools, the Coverage tab shows how much of each stylesheet and script the page used and how much it only downloaded. A file that is almost wholly unused on a page is a candidate for removal there.

What defer and async do

Both attributes tell the browser to fetch a script while it carries on reading the page. They differ in when the script runs.

The script tag hasWhen the script runsOrder
Neither attributeAt once. The browser stops reading the page until it has run.As written in the page
deferAfter the whole page has been read, before the DOMContentLoaded eventAs written in the page
asyncAs soon as it has downloaded, wherever the browser has got toNot guaranteed

defer is the safe choice for most scripts, because deferred scripts still run in the order they appear. A script that needs another one finds it already there, as long as both are deferred.

async suits only a script that depends on nothing and that nothing depends on.

Neither attribute does anything on an inline script, the kind written straight into the page with no src. Inline code runs the moment the browser reaches it. That is where most breakage starts.

Why deferring breaks pages: jQuery and inline scripts

WordPress ships jQuery under the handle jquery, and a theme or plugin that uses it lists that handle as a dependency so jQuery is printed first. Plugins also print short pieces of inline script that call jQuery there and then.

Defer jQuery and those inline pieces run before it does. The browser's console shows a "jQuery is not defined" error, and whatever that code was meant to start never starts.

Optimization plugins deal with this in different ways. Autoptimize's listing says it leaves WordPress's jQuery file out of its JavaScript optimization by default, because so many plugins print inline code that relies on it. WP Rocket's documentation says its defer option defers jQuery too, and wraps the inline scripts it recognizes as using jQuery so they run on DOMContentLoaded. The same page says it is sometimes not possible to defer all JavaScript without breaking the site.

WordPress's own loading strategies guard against the same thing. A deferred script's dependents must be deferred as well. If one of them is blocking, WordPress keeps the script and its dependencies blocking. It does the same for a script that has inline code attached after it with wp_add_inline_script(). What WordPress cannot see is inline code that a theme or plugin prints into the page by itself.

Three ways to fix it

Whichever you choose, do it on a staging site first.

A setting in an optimization plugin

An optimization plugin rewrites the tags as the page is sent, so it reaches every file without anyone editing code. Two examples, as their makers document them:

  • WP Rocket needs a license. On its File Optimization tab, Load JavaScript deferred adds defer to every script file on the page. Delay JavaScript execution holds all scripts back until the visitor moves the mouse, touches the screen, scrolls or presses a key. Under Optimize CSS delivery there are two choices that cannot be on together. Remove Unused CSS keeps only the CSS each page uses and puts it in the page. Load CSS Asynchronously puts critical CSS in the page and loads the stylesheets without blocking.
  • Autoptimize is free in the WordPress.org directory, with a paid add-on called Autoptimize Pro. Its listing says JavaScript is made non-render-blocking by default, and CSS stays linked in the head unless you choose its "inline and defer CSS" option and give it the styles for the top of the page.

Use one tool for this job. Autoptimize's listing says not to run another minification plugin beside it, and to switch the same functions off in a caching plugin. Both plugins have exclusion lists, so when one file breaks the page you can leave that file out and keep the setting. WordPress caching plugins compared covers what each caching plugin does beyond the page cache.

Register the script with a loading strategy

Since WordPress 6.3, the last argument of wp_register_script() and wp_enqueue_script() can be an array with a strategy key, set to defer or async. This is the route for a script of your own. The code goes in the functions.php of a child theme, or in a small plugin.

functions.php
add_action( 'wp_enqueue_scripts', 'example_child_enqueue_scripts' );

function example_child_enqueue_scripts() {
	wp_enqueue_script(
		'example-child-menu',
		get_stylesheet_directory_uri() . '/js/menu.js',
		array(),
		'1.0.0',
		array(
			'strategy' => 'defer',
		)
	);
}
Loads js/menu.js from the child theme's folder, deferred.

Adding 'in_footer' => true to that array prints the tag at the end of the page instead of in the head.

For a script that a plugin or the parent theme registers, set the strategy on its handle with wp_script_add_data(). The function only works on a script that is already registered, so the 100 makes it run late.

functions.php
add_action( 'wp_enqueue_scripts', 'example_child_defer_script', 100 );

function example_child_defer_script() {
	wp_script_add_data( 'example-slider', 'strategy', 'defer' );
}
Replace example-slider with the handle you found in the page's source.

To check, view the page's source and find the tag. It should now carry defer. If it carries data-wp-strategy="defer" but no defer, WordPress kept the script blocking for one of the reasons in the section above. If nothing changed, the script is registered later than this code runs.

Older code adds the attribute by rewriting the tag in the script_loader_tag filter. WordPress's note on the 6.3 change calls that "hacky", because it takes no account of dependencies or inline scripts.

Stop loading a file the page does not use

A file that is not loaded cannot block. If a plugin loads its script and stylesheet on every page and only one page uses them, remove them everywhere else.

functions.php
add_action( 'wp_enqueue_scripts', 'example_child_remove_form_assets', 100 );

function example_child_remove_form_assets() {
	if ( is_page( 'contact' ) ) {
		return;
	}

	wp_dequeue_script( 'example-form' );
	wp_dequeue_style( 'example-form' );
}
Keeps a form plugin's files on the page with the slug contact and removes them elsewhere.

Check every page where the feature appears, including any widget or pop-up that shows it. If a file still loads, another script or stylesheet may list it as a dependency. A plugin the site no longer uses needs no code: delete it.

Stylesheets are harder than scripts

A script can wait. The CSS for the first view cannot, because without it the page appears unstyled and then jumps into shape. There are three things you can do, in order of risk.

Load less CSS. Removing a stylesheet a page does not use, as above, is the safe gain.

Give a stylesheet a media value. The browser does not hold up drawing for a stylesheet whose media does not match the device, though it still downloads it. WordPress prints media='all' unless the code that loads the stylesheet says otherwise, through the last argument of wp_enqueue_style(). A print stylesheet loaded with print there does not block the first view.

Critical CSS. The idea is to put the styles the top of the page needs into the page itself, and load the full stylesheets afterwards. Plugins do it by turning each stylesheet link into a preload that becomes a stylesheet once it has arrived. WP Rocket's documentation says its external tool works out the critical CSS for each kind of page, and that you should regenerate it after changing stylesheets or custom CSS.

Doing this by hand is fragile. Autoptimize's listing puts it plainly: what is above the fold depends on where the fold is, and that depends on the size of the screen. The CSS you paste in also stays as it was while the theme and its plugins change. Chrome's documentation calls inlining CSS an advanced technique that can lead to bugs, and says most sites can reach its recommended targets without it. If the audit lists only your theme's main stylesheet, leaving it there is a reasonable choice.

Test after every change

  1. Step 1: Work on a staging copy

    Make the change on a staging site. On the live site, take a backup first.

  2. Step 2: Change one thing

    One setting, or one handle. If two things change and the page breaks, you cannot tell which did it.

  3. Step 3: Clear every cache

    Clear the optimization plugin's cache, the page cache, and the host's or CDN's cache if there is one. Otherwise you are looking at the old page.

  4. Step 4: Open the page logged out, with the console open

    Use a private window, so you see what a visitor sees. Open the browser's developer tools on the console tab and reload. A new error, such as "jQuery is not defined", points at the change you just made.

  5. Step 5: Use the page

    Open the menu in a phone-sized window. Submit a form. Move a slider. On a store, add a product to the cart and go to the checkout. Watch the page as it loads for a moment without its styles.

  6. Step 6: Run the lab test again

    The file should be gone from the audit. Run the test more than once, because lab results vary from one run to the next.

How to undo a change

  • A plugin setting. Switch it off, or add the file that broke to the plugin's exclusion list. Then clear the plugin's cache and the page cache. Autoptimize's listing warns that a cached page can still point at optimized files that no longer exist, and the page looks broken until the page cache is cleared too.
  • Code. Remove the lines you added and clear the caches. Edit functions.php through the host's file manager or SFTP, and keep a copy of the version that worked. If the site goes blank after an edit, put that copy back or follow how to fix the white screen of death.
  • Critical or used CSS. Switch the option off and clear what the plugin generated, so pages go back to their ordinary stylesheets.

What clearing the audit does not do

  • It does not set the score. Lighthouse works out the performance score from metrics. The lists of suggestions do not count toward it.
  • Its time savings are estimates. Any saving the report shows beside a file comes from one simulated load of the page. Treat it as a guide to which files matter most, not as time your visitors will get back.
  • It does not make scripts cheaper. A deferred script still downloads and still runs. Loading less is the lasting fix.
  • It does not fix a slow server or heavy images. Those come first, in the order how to speed up WordPress sets out.

If you would rather hand the work over, our speed optimization service is a one-time job: you name the pages, and we measure them before the work and again after it.

Common questions

Will clearing this audit raise my PageSpeed score?

It can, but not directly. The score comes from metrics such as Largest Contentful Paint. If the blocking files were delaying the first view, that metric improves and the score follows. If they were not, the list gets shorter and the score stays where it was.

Should I defer jQuery?

Only if you are ready to check every page. With WordPress's own strategies, jQuery can be deferred only when every script that depends on it is deferred too, and inline code that calls jQuery will still fail. An optimization plugin that defers jQuery has to rewrite that inline code as well. Leaving jQuery blocking and deferring the scripts that depend on it is the cautious choice.

Is it safe to delay all JavaScript until the visitor does something?

It changes how a page behaves. Nothing a script draws or starts is there until the first movement, touch or key press, so a slider or a chat button can appear late. WP Rocket's documentation offers exclusions for this, and a safe mode meant as a temporary measure while you find the files to exclude. Check every kind of page before you leave it on.

I deferred everything and the audit still lists files. Why?

Usually they are stylesheets, which defer does not touch, or files you left out on purpose so the page keeps working. An audit that still lists a stylesheet the first view needs is not a fault. Look at what the page's metrics say.

More on this subject

Speed optimization, done for you

Speed optimization is $149. Before-and-after measurements on named pages. It starts with a free diagnosis.