All rules

Sheath rule

perf-no-render-blocking

Scripts should use async or defer to avoid blocking page rendering.
Package
Core
Category
Performance
Default severity
warning by default
Auto-fix
Auto-fix available

(dangerous)

#Why

A plain <script src> stops HTML parsing until the file has been fetched and run. In <head>, or before renderable content in <body>, nothing below it is parsed, so nothing below it can be painted. Several such scripts also run one after another rather than together.

defer fetches in parallel and runs after parsing, in document order. async fetches in parallel and runs as soon as it arrives, in no guaranteed order.

#Examples

#Bad

<head>
<!-- Render-blocking scripts -->
<script src="/js/analytics.js"></script>
<script src="/js/widget.js"></script>
<script src="/js/app.js"></script>
<script type="module" blocking="render" src="/js/module.js"></script>
</head>

Before the document body starts, an explicit blocking="render" token is reported even when a script also uses async, defer, or type="module". Adding defer does not neutralize that effective token, so the rule does not offer an automatic fix for this case. After <body> starts, the browser cannot add a script to the render-blocking set, so an otherwise non-blocking body script is not reported merely for carrying the token.

#Good

<head>
<!-- Async for independent scripts -->
<script src="/js/analytics.js" async></script>
<!-- Defer for DOM-dependent scripts -->
<script src="/js/app.js" defer></script>
<!-- Module scripts are deferred by default -->
<script src="/js/module.js" type="module"></script>
</head>
<!-- Or place scripts at end of body -->
<body>
<!-- content -->
<script src="/js/app.js"></script>
</body>

#async vs defer

Choose an attribute based on whether execution order matters.

Attribute Download Execution Use Case
(none) Blocks parsing Immediately Critical inline scripts
async Parallel When ready Independent scripts (analytics)
defer Parallel After DOM parse DOM-dependent scripts
type="module" Parallel After DOM parse ES modules

#Visual Timeline

Without async/defer:
HTML: ═══════░░░░░░░░░░░░░═══════════
↑ blocked ↑ resumes
Script: [download][execute]
With async:
HTML: ═══════════════════════════════
Script: [download][execute]
↑ may interrupt
With defer:
HTML: ═══════════════════════════════●
Script: [download] [execute]
↑ after parsing, before DOMContentLoaded

#Options

Use this option to exempt selected script URLs.

Option Type Default Description
excludePatterns array [] URL patterns to exclude
<?php
'perf-no-render-blocking' => ['warning', [
'excludePatterns' => ['critical.js'],
]],

#Auto-fix

The fixer adds defer, and the fix is dangerous: it is skipped by a plain --fix and applied only under --dangerous.

<!-- Before -->
<script src="/js/app.js"></script>
<!-- After -->
<script src="/js/app.js" defer></script>

defer does not just delay the download. It moves the script's execution to after parsing, so anything that used to run between this tag and the end of the document now runs first:

<head>
<script src="/js/analytics.js"></script>
<script>analytics.track('view');</script> {{-- breaks once the first is deferred --}}
</head>

Dependencies between scripts cannot be determined from the template. Review the diff, or use async manually where execution order does not matter.

#What's Skipped

  • Inline scripts (no src attribute)
  • Scripts with type="module" (deferred by default)
  • Non-JavaScript types (JSON-LD, etc.)
  • Scripts at the end of <body>, when only scripts, comments, whitespace, and non-rendering Blade control syntax follow
  • Scripts inside capture-only @section, @push, @pushIf, @prepend, and once variants. Their output location is determined by the layout's @yield or @stack, so this file cannot determine whether the script blocks rendering. A section closed with @show renders at its definition site and is still checked
<!-- Not reported: renders at the layout's @stack('scripts') -->
@push('scripts')
<script src="/js/chart.js"></script>
@endpush

#Notes

  • Checks apply to scripts in <head> and body scripts that precede renderable content
  • Scripts at the end of <body> don't need async/defer
  • Use defer when script order matters, async when it doesn't
  • JavaScript MIME essence matching follows HTML exactly: it is case-insensitive, but surrounding whitespace or parameters make the value a non-JavaScript data-block type

#References

#Related Rules