Fixing Render-Blocking JavaScript
When a script sits in the page head without being told it can wait, the browser stops building the page, fetches the script, runs it, and only then carries on. On a phone with a weak signal that pause can leave visitors looking at a blank screen for seconds.
This guide is only about how scripts are delivered: finding the ones that block the first paint, deciding which loading method each should use, fixing the inline code that breaks when you change the order, and dealing with the few snippets that genuinely try to hold the page back. Stylesheets, bundle size and slow code have their own guides.
What makes a script block rendering
The HTML parser reads your page from top to bottom. When it meets a classic script tag with a src
and no async, defer or type="module", it has to assume the script
might change what comes next, for example by writing into the page. So it pauses: nothing below the tag
is parsed until the file has downloaded and run. When that tag is in the head, nothing in the body exists
yet, so there is nothing to paint.
Three details make this worse than it sounds:
- Blocking scripts queue behind stylesheets. A classic script will not run until every stylesheet above it has loaded, because the script might ask for computed styles. A slow stylesheet therefore delays the script, and the script delays everything after it.
- Each one adds a round trip. Browsers scan ahead and start downloading later files early, which helps, but every blocking file still has to arrive and execute in order before parsing resumes.
- Other domains cost more. A script on a domain the page has not contacted yet needs a DNS lookup and a secure connection before its first byte.
Inline scripts, written directly in the page, do not need a download, but they still stop the parser while they run, and they still wait for stylesheets above them. A long inline script in the head blocks just like an external one.
defer, async and module: what each one changes
| Tag | Parser waits? | Runs when | Order kept? | Use it for |
|---|---|---|---|---|
| Classic script, no attribute | Yes, for download and run | Immediately | Yes | Almost nothing in the head; very small inline code that must run first |
defer | No | After parsing finishes, before DOMContentLoaded | Yes, in document order | Site scripts, libraries and anything that uses the page |
async | Only while it runs | As soon as it has downloaded | No | Self-contained scripts with no dependants: analytics, tag managers, some widgets |
type="module" | No | Like defer | Yes | Modern
code using import |
type="module" async | Only while it runs | As soon as it and its imports are ready | No | Independent modules |
Some rules that trip people up:
deferandasynconly work on scripts with asrc. On a classic inline script they are ignored and the code runs straight away. Inline module scripts are the exception: they are deferred automatically.- If a tag has both
asyncanddefer, browsers that understandasyncuse it. There is no reason to write both today. - Deferred scripts all run before the
DOMContentLoadedevent fires, so code waiting for that event can safely use anything a deferred script defines. - Moving a script to the end of the body helps the content above it appear, but the parser still stops
there. Adding
deferin the head is usually better, because the download starts earlier.
How to find the blocking scripts
The JavaScript Checker flags render-blocking scripts on each page you test and lists every script with its size and parse and execution time, so you can see which blockers are also expensive to run. PageSpeed Insights reports the same problem in its render-blocking requests insight, listing stylesheets and scripts together with an estimate of the delay; read the file names to separate the two.
In Chrome, you can ask the browser directly which script files it treated as blocking on the load you just made:
performance.getEntriesByType('resource')
.filter((r) => r.initiatorType === 'script' && r.renderBlockingStatus === 'blocking')
.forEach((r) => console.log(r.name, Math.round(r.responseEnd - r.startTime), 'ms'));
Then view the page source, not the Elements panel, and search for <script in the head.
For every tag, note whether it has a src, whether it has defer, async
or a module type, and whether any inline code below it depends on it. That list is your working plan.
On CMS sites, check several templates: home, category, product and blog posts often load different plugin
scripts.
curl, shows the tags the parser actually
met.The fix, step by step
Step 1
List the scripts that block the first paint
Run the page through the JavaScript Checker and note every script it flags as render-blocking,
with its size and execution time. Then open the page source and write down each script tag in the head
and body: its address, whether it is inline, and which of defer, async or type="module"
it has. Mark any inline code that calls a function from one of those files.
Step 2
Decide what truly must run before first paint
Very little belongs before the first paint: a tiny inline snippet that sets a class for a saved dark theme to prevent a flash, a consent default that must exist before any tag runs, or a small feature check. Keep those inline and short. Everything else, including sliders, menus, forms, analytics, chat and reviews, can wait until after the page is visible and parsed.
Step 3
Defer scripts that work with the page
Add defer to every external script that reads or changes the page, including
libraries such as jQuery and the scripts that depend on them. Keep them in the same order, since deferred
scripts run in document order, and leave them in the head so the downloads start early. Test menus, forms,
galleries and the basket after the change, because this is where hidden dependencies show up.
Step 4
Reserve async for independent scripts
Use
async only for scripts that nothing else calls and that do not need the page to be complete,
such as analytics or a tag manager container. Never make a library async if other code depends on it,
because it may run before or after its dependants at random. When unsure, choose defer,
which is almost never slower for first paint and keeps the order predictable.
Step 5
Rewire inline code that calls deferred libraries
Inline scripts run immediately, so an inline call to a deferred library fails with a
not defined error. Move that code into its own file loaded with defer after the library,
or wrap it in a DOMContentLoaded listener, which fires only after deferred scripts have
run. For jQuery ready calls in themes, the listener wrapper is usually the quickest safe fix.
Step 6
Remove document.write and page-hiding snippets
Replace any snippet that uses document.write to add scripts with the vendor's
current asynchronous loader, since Chrome may block such scripts on slow connections anyway. Look for
A/B testing or personalisation snippets that hide the page until a script loads, often called anti-flicker
code, and either remove them, cut their timeout sharply, or limit them to the pages running a live test.
Step 7
Preload critical scripts the browser finds late
If a script needed early is only discovered late, for example because another script
adds it or it is imported by a module several levels deep, add link rel="preload" as="script"
or link rel="modulepreload" in the head so the download starts straight away. Preload only
one or two genuinely important files, because every preload competes with the main image and stylesheets
for bandwidth.
Step 8
Check first paint on a throttled connection
In the Performance panel, set network throttling to a slow 4G profile and CPU throttling to 4x, record a reload before and after your changes, and compare when the first paint happens. Re-run the JavaScript Checker to confirm no scripts are still flagged as render-blocking, then click through the key journeys on a real phone to make sure nothing broke.
Before and after: a typical theme head
This head, based on a common older theme pattern, blocks the first paint three times: once for the library, once for the slider plugin and once for the inline code.
<!-- Before -->
<head>
<link rel="stylesheet" href="/css/site.css">
<script src="/js/jquery.min.js"></script>
<script src="/js/slider.min.js"></script>
<script>
$(function () { $('.hero-slider').slider({ speed: 400 }); });
</script>
<script src="https://widgets.example.net/reviews.js"></script>
</head>
<!-- After -->
<head>
<link rel="stylesheet" href="/css/site.css">
<script src="/js/jquery.min.js" defer></script>
<script src="/js/slider.min.js" defer></script>
<script src="/js/home.js" defer></script>
<script src="https://widgets.example.net/reviews.js" async></script>
</head>
// /js/home.js - runs after jQuery and the slider, in order
$('.hero-slider').slider({ speed: 400 });
The inline code moved into a small deferred file listed after the libraries it needs. The reviews widget depends on nothing and nothing depends on it, so it became async. If you cannot move inline code into a file, for example because a CMS prints it, wrap it instead:
document.addEventListener('DOMContentLoaded', function () {
$('.hero-slider').slider({ speed: 400 });
});
Snippets that deliberately hold the page back
A few kinds of script are designed to run before the page shows. Each is worth questioning.
A/B testing and personalisation
Client-side testing tools change the page after it loads, so they often ship an anti-flicker snippet that hides the whole page until the testing script has run or a timeout expires. That can add seconds of blank screen for every visitor, including those not in any test. Options, from best to least effective: run tests on the server or at the CDN edge instead; load the snippet only on pages with a live test; or cut the timeout to the shortest value the vendor supports. Remove the snippet completely when a testing tool is retired; old anti-flicker code for discontinued tools is still found on many sites.
Consent tools
Consent management platforms usually ask to be placed first in the head. If you use Google consent mode, the small inline block that sets the default consent state does need to run before tags load, and it is tiny. The banner script itself rarely needs to block; check whether your provider offers an asynchronous loading option and test that tags still respect the stored choice when you use it. This is not legal advice; check your consent set-up with whoever is responsible for compliance before changing the order.
document.write
Older advertising and widget snippets use document.write to insert further scripts. Those
inserted scripts block the parser, and Chrome may refuse to load cross-origin scripts inserted this way
for users on slow connections, so the snippet can both slow the page and fail. Browsers also ignore document.write
called from a deferred or async script. Ask the vendor for their current loader.
Platform notes
WordPress
Since WordPress 6.3, themes and plugins can ask for a loading strategy when they register a script,
by passing an array such as array( 'strategy' => 'defer', 'in_footer' => false ) as
the last argument of wp_register_script or wp_enqueue_script. WordPress then
works out a safe strategy based on dependencies, falling back to blocking if a dependant needs it. Themes
and plugins that do not use this still print blocking tags; performance plugins can add defer
for you, but exclude jQuery and anything that inline code calls if pages break, and test page builders
carefully.
Shopify
The script_tag Liquid filter outputs a plain blocking tag. In theme.liquid
and section files, write the tag yourself with the asset_url filter for the address and
add defer. Scripts from apps arrive through app embeds and content_for_header;
you cannot edit those tags directly, so switch off app embeds you do not use and raise blocking scripts
with the app developer.
Frameworks and static sites
Bundlers such as Vite output module scripts, which are deferred by default, so the main bundle rarely
blocks. Blocking tags usually come from third-party snippets pasted into the base template. In Next.js,
load those through next/script with the afterInteractive or lazyOnload
strategy rather than a raw tag in the document head.
Edge cases
- A flash of the wrong theme or layout. Some small scripts exist to set a class before paint, for a dark mode preference or a no-JavaScript fallback. Keep these inline and tiny rather than deferring them.
- Scripts inside the body. A blocking tag halfway down the body lets the content above it render, but stops everything below, including images further down. Treat it like one in the head.
- The blocking attribute. Chromium supports a
blocking="render"attribute that makes a script deliberately render-blocking. If you find it in a template, check that someone meant it. - Deferring does not make code faster. Once a deferred script runs, it takes as long as it always did. If the page appears quickly but stays unresponsive, the problem has moved to long tasks, not been solved.
1. What is render-blocking JavaScript?
It is any script the browser must download and run before it can carry on building and showing the page. In practice that means a classic script tag in the HTML with a src and no defer, async or module type, plus any inline script in the head. While the browser waits, the visitor sees a blank or partly drawn screen. Adding defer or async, or moving the code, lets the page appear first and the script run afterwards.
2. Should I use defer or async?
Use defer for almost everything that works with the page or depends on another script, because deferred scripts wait until the HTML is parsed and then run in the order they appear. Use async only for self-contained scripts that nothing else calls, such as analytics or a tag manager, since async scripts run as soon as they arrive, in any order. If you are unsure, defer is the safer choice and is rarely any slower for the first paint.
3. Why did adding defer break my site?
Usually because inline code somewhere on the page calls a function from the script you deferred. Inline scripts run immediately, before deferred scripts, so the function does not exist yet and the console shows a not defined error. Move that inline code into a small deferred file listed after the library, or wrap it in a DOMContentLoaded event listener, which only fires after all deferred scripts have run. Then test menus, forms and sliders again.
4. Is putting scripts at the bottom of the page enough?
It helps, because content above the script can be parsed and shown first, and for years it was the standard advice. But the parser still stops at the script, so anything after it waits, and the download only starts when the parser reaches the tag. Keeping the script in the head with defer usually does better, because the file starts downloading straight away while the parser carries on, and it still runs after the HTML is ready.
5. Do Google Tag Manager and analytics block rendering?
The standard loader snippets for Google Tag Manager and Google Analytics insert their main script asynchronously, so they do not block the parser, although the small inline part of the snippet runs where it is placed. They can still slow the page later by running a lot of code once loaded, which is a separate problem from render-blocking. Tags added inside the container that use document.write or hide the page are the ones to review.
6. Is it safe to defer jQuery?
Yes, if everything that uses jQuery is also deferred or waits for the DOMContentLoaded event. Deferred scripts run in document order, so jQuery followed by plugins that need it works as before. What breaks is inline code in templates or page builder blocks that calls jQuery immediately. On WordPress, find those calls first, or exclude jQuery from deferral in your performance plugin if a page builder relies on inline calls you cannot change.
7. What is an anti-flicker snippet and should I remove it?
It is a piece of code supplied by some client-side A/B testing and personalisation tools that hides the whole page until the testing script has loaded or a timeout passes, so visitors do not see the original version change. It delays the first paint for everyone, including visitors not in a test. Remove it if the tool is no longer used, limit it to pages with a live test, shorten the timeout, or move testing to the server.
The JavaScript Checker loads your page in headless Chromium, flags render-blocking scripts, and lists every script with its size, parse and execution time, alongside third-party tags and console errors.
Open JavaScript Checker Fixes โ