A single misplaced script tag can block rendering for seconds; the right attribute fixes it for free.
A plain <script src="app.js"> in the head is parser-blocking: the browser stops building the page, downloads the script, runs it, then continues. Users stare at a blank screen meanwhile. Two attributes fix this for external scripts.
defer: download in parallel with parsing, run after the document is parsed, in document order, beforeDOMContentLoaded. Right for almost all app code, because the DOM is ready when it runs.async: download in parallel and run as soon as it arrives, in any order, possibly before parsing finishes. Right for independent scripts like analytics.
<script type="module"> loads an ES module. Modules are deferred by default, run in strict mode, can import other modules, and run once even if included twice. Add async to a module to run it as soon as it and its imports are ready. <script nomodule> is a fallback for browsers without module support, which today is effectively none.
Inline scripts without src ignore defer and run immediately (unless they're modules).
Resource hints in the head speed up loading:
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>: fetch something needed soon that the browser would discover late.<link rel="modulepreload" href="/js/chart.js">: preload a module and its parsing.<link rel="preconnect" href="https://cdn.example">: open the connection early.
Overusing preload backfires: everything preloaded competes for bandwidth with what's actually critical.
Example
<head>
<!-- Blocks parsing until downloaded and run: avoid -->
<!-- <script src="/js/app.js"></script> -->
<!-- Runs after parsing, in order: good default for app code -->
<script src="/js/vendor.js" defer></script>
<script src="/js/app.js" defer></script>
<!-- Runs whenever it arrives: independent third-party code -->
<script src="https://analytics.example/a.js" async></script>
<!-- ES module: deferred automatically -->
<script type="module" src="/js/main.js"></script>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
</head>Remember
- Plain scripts in the head block parsing
- defer = after parsing, in order; async = whenever ready, any order
- Module scripts are deferred by default
- Preload only the few resources that are critical and discovered late
Watch out for
- Using async for scripts that depend on each other; their order isn't guaranteed.