Eliminating Render-Blocking JavaScript: The Easiest Core Web Vitals Win You’re Not Taking

If you’ve run your site through Google PageSpeed Insights and seen the “Eliminate render-blocking resources” warning, you’ve probably wondered why something that sounds so simple is so hard to actually fix. The answer is that WordPress makes it surprisingly easy to load JavaScript the wrong way — and surprisingly difficult to fix it without either a heavyweight performance plugin or hand-editing your theme.

CloudScale SEO AI Optimizer v4.9.4 adds a single checkbox that sorts this out.

What “render-blocking” actually means

When a browser parses your HTML and encounters a <script src="..."> tag, it stops. It fetches the script, executes it, and only then continues rendering the page. Every external JavaScript file loaded this way adds its full network round-trip time to your Time to First Contentful Paint — the metric Google uses most heavily in its Core Web Vitals assessment.

The fix is the defer attribute:

<!-- Blocks rendering — browser stops here and waits -->
<script src="/wp-content/plugins/something/script.js"></script>

<!-- Non-blocking — browser continues parsing HTML, runs script after -->
<script defer src="/wp-content/plugins/something/script.js"></script>

With defer, the browser downloads the script in parallel with HTML parsing and only executes it once the DOM is fully built. Your page paints immediately. The scripts still run, in order, shortly after — but by then the user is already looking at content.

Why not just use async?

async is the other non-blocking option and it sounds appealing, but it executes scripts the moment they finish downloading regardless of order. If script B downloads before script A but depends on it, things break. Most WordPress sites have jQuery as a dependency chain root — async would have it executing at an unpredictable moment, breaking virtually every theme and plugin that calls $(document).ready().

defer preserves execution order while still being non-blocking. It’s almost always the right choice.

The new toggle

In Settings → Optimise SEO → Features & Robots, there’s a new checkbox: Defer render-blocking JavaScript.

Enable it, save, and the plugin hooks into WordPress’s script_loader_tag filter — which intercepts every script tag WordPress outputs via its asset pipeline — and adds defer to each one.

A few scripts are excluded automatically because deferring them reliably breaks things:

  • jQuery and its migration shim
  • WooCommerce cart and checkout scripts
  • reCAPTCHA and hCaptcha
  • Elementor frontend
  • wp-embed

If something breaks after enabling the toggle, there’s an exclusions textarea where you can add script handle names or URL substrings — one per line — to restore normal loading for just that script without turning off the feature globally.

Verifying it works

The fastest way to confirm the change is working is with this one-liner:

cat > ./check-defer.js << 'EOF'
#!/usr/bin/env node
const https=require('https'),http=require('http'),url=require('url');
const args=process.argv.slice(2),verbose=args.includes('--verbose')||args.includes('-v'),target=args.find(a=>a.startsWith('http'));
if(!target){console.error('Usage: node check-defer.js <url> [--verbose]');process.exit(1);}
const C={reset:'\x1b[0m',bold:'\x1b[1m',green:'\x1b[32m',red:'\x1b[31m',yellow:'\x1b[33m',cyan:'\x1b[36m',grey:'\x1b[90m'};
const ok=`${C.green}✓${C.reset}`,fail=`${C.red}✗${C.reset}`,skip=`${C.grey}–${C.reset}`,warn=`${C.yellow}!${C.reset}`;
const SAFE_EXCLUSIONS=['jquery','jquery-migrate','jquery-core','wp-embed','wc-checkout','wc-cart','wc-add-to-cart','recaptcha','hcaptcha','google-tag-manager','gtag','elementor-frontend'];
function isIntentionallyExcluded(src){if(!src)return false;const s=src.toLowerCase();return SAFE_EXCLUSIONS.some(e=>s.includes(e));}
function fetch(u,r=0){return new Promise((res,rej)=>{if(r>5)return rej(new Error('Too many redirects'));const p=url.parse(u),lib=p.protocol==='https:'?https:http;const req=lib.request({hostname:p.hostname,port:p.port||(p.protocol==='https:'?443:80),path:p.path||'/',method:'GET',headers:{'User-Agent':'check-defer/1.0','Accept':'text/html'}},re=>{if([301,302,303,307,308].includes(re.statusCode)&&re.headers.location){const next=re.headers.location.startsWith('http')?re.headers.location:`${p.protocol}//${p.hostname}${re.headers.location}`;return fetch(next,r+1).then(res).catch(rej);}let b='';re.setEncoding('utf8');re.on('data',c=>{b+=c;});re.on('end',()=>res({status:re.statusCode,body:b}));});req.on('error',rej);req.end();});}
function parseScripts(html){const out=[];const re=/<script([^>]*)>/gi;let m;while((m=re.exec(html))!==null){const a=m[1],srcM=/\bsrc=["']([^"']+)["']/i.exec(a),typeM=/\btype=["']([^"']+)["']/i.exec(a),type=typeM?typeM[1].toLowerCase():null;out.push({src:srcM?srcM[1]:null,hasDefer:/\bdefer\b/i.test(a),hasAsync:/\basync\b/i.test(a),isNonJs:!!(type&&type!=='text/javascript'&&type!=='module'&&!type.includes('javascript'))});}return out;}
function short(src,base){if(!src)return'(inline)';try{const u=new url.URL(src,base),bh=new url.URL(base).hostname;return u.hostname===bh?u.pathname+(u.search||''):u.hostname+u.pathname;}catch{return src.slice(0,80);}}
(async()=>{
console.log('\ncheck-defer.js — Render-Blocking Script Audit');
console.log('─'.repeat(60));
console.log('Fetching: '+target+'\n');
let res;try{res=await fetch(target);}catch(e){console.error('Failed to fetch: '+e.message);process.exit(1);}
if(res.status!==200){console.error('HTTP '+res.status);process.exit(1);}
const scripts=parseScripts(res.body);
if(res.body.includes('CloudScale SEO'))console.log('✓ CloudScale SEO AI Optimizer detected\n');
const ext=scripts.filter(s=>s.src&&!s.isNonJs);
const inline=scripts.filter(s=>!s.src&&!s.isNonJs);
const nonJs=scripts.filter(s=>s.isNonJs);
const deferred=ext.filter(s=>s.hasDefer);
const asyncd=ext.filter(s=>s.hasAsync&&!s.hasDefer);
const blockingAll=ext.filter(s=>!s.hasDefer&&!s.hasAsync);
const blockingExpected=blockingAll.filter(s=>isIntentionallyExcluded(s.src));
const blockingUnexpected=blockingAll.filter(s=>!isIntentionallyExcluded(s.src));
console.log('External scripts');
console.log('─'.repeat(60));
console.log('  Total:                        '+ext.length);
console.log('  ✓ Deferred:                  '+deferred.length);
console.log('  ✓ Async (self-managed):       '+asyncd.length);
console.log('  ! Intentionally not deferred: '+blockingExpected.length+'  (safe — excluded by design)');
console.log('  '+(blockingUnexpected.length>0?'✗':'✓')+' Unexpected render-blocking:  '+blockingUnexpected.length+'\n');
if(blockingExpected.length>0){
  console.log('Intentionally not deferred: (plugin excludes these on purpose)');
  blockingExpected.forEach(s=>console.log('  ! '+short(s.src,target)));
  console.log();
}
if(blockingUnexpected.length>0){
  console.log('⚠  Unexpected blocking scripts (action needed):');
  blockingUnexpected.forEach(s=>console.log('  ✗ '+short(s.src,target)));
  console.log();
}
if(verbose&&deferred.length>0){console.log('Deferred scripts:');deferred.forEach(s=>console.log('  ✓ '+short(s.src,target)));console.log();}
if(verbose&&asyncd.length>0){console.log('Async scripts:');asyncd.forEach(s=>console.log('  ✓ '+short(s.src,target)));console.log();}
console.log('─'.repeat(60));
console.log('  – '+inline.length+' inline block(s) — cannot be deferred (expected)');
if(nonJs.length>0)console.log('  – '+nonJs.length+' non-JS block(s) — JSON-LD etc., ignored');
console.log('\n'+'─'.repeat(60));
if(blockingUnexpected.length===0){
  console.log('✓ PASS — defer is working correctly');
  if(deferred.length>0)console.log('   '+deferred.length+' script(s) successfully deferred');
  if(blockingExpected.length>0)console.log('   '+blockingExpected.length+' script(s) intentionally not deferred (e.g. jQuery) — this is correct');
}else{
  console.log('✗ FAIL — '+blockingUnexpected.length+' unexpected render-blocking script(s)');
  console.log('\n   Either enable "Defer render-blocking JavaScript" in');
  console.log('   Settings → Optimise SEO → Features & Robots,');
  console.log('   or these scripts may be loaded outside wp_enqueue_script.');
}
if(!verbose&&deferred.length>0)console.log('\nTip: run with --verbose to list all deferred scripts');
console.log();
})();
EOF
chmod +x ./check-defer.js && node ./check-defer.js https://andrewbaker.ninja

Run it before and after enabling the toggle. A passing result looks like:

check-defer.js — Render-Blocking Script Audit
────────────────────────────────────────────────────────────
Fetching: https://andrewbaker.ninja

(node:52863) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
(Use `node --trace-deprecation ...` to show where the warning was created)
✓ CloudScale SEO AI Optimizer detected

External scripts
────────────────────────────────────────────────────────────
  Total:                        5
  ✓ Deferred:                  3
  ✓ Async (self-managed):       0
  ! Intentionally not deferred: 2  (safe — excluded by design)
  ✓ Unexpected render-blocking:  0

Intentionally not deferred: (plugin excludes these on purpose)
  ! /wp-includes/js/jquery/jquery.min.js
  ! /wp-includes/js/jquery/jquery-migrate.min.js

────────────────────────────────────────────────────────────
  – 6 inline block(s) — cannot be deferred (expected)
  – 4 non-JS block(s) — JSON-LD etc., ignored

────────────────────────────────────────────────────────────
✓ PASS — defer is working correctly
   3 script(s) successfully deferred
   2 script(s) intentionally not deferred (e.g. jQuery) — this is correct

Tip: run with --verbose to list all deferred scripts

One thing this won’t fix

Inline scripts — blocks of JavaScript written directly into the HTML rather than loaded from an external file — cannot be deferred. The defer attribute only applies to external file loads, and WordPress’s script_loader_tag filter only fires for those. Inline scripts from page builders or themes that call document.write() or manipulate the DOM immediately are a separate problem requiring a different approach. For most standard WordPress blogs, however, the bulk of JavaScript is enqueued externally, and this change will clear the PageSpeed warning entirely.

The feature is free, in the plugin, and takes about three seconds to enable.

Leave a Reply

Your email address will not be published. Required fields are marked *