Script Loading & Page Lifecycle Events
The order in which scripts load and page lifecycle events fire is one of the most commonly asked browser fundamentals. Getting it wrong means render-blocking scripts, flash of unstyled content, or JavaScript executing before the DOM exists.
Script Loading Strategies
Default (No Attribute)
<script src="app.js"></script>HTML parsing: âââââââââââââââââââââââââââââââ
Script fetch: ââââââââ
Script execute: ââââ
â HTML parsing PAUSEDThe parser stops when it encounters a <script> tag, downloads the script, executes it, then resumes parsing. This is render-blocking.
async
<script src="analytics.js" async></script>HTML parsing: ââââââââââââââââââââââââââââââââ
Script fetch: ââââââââ
Script execute: ââââ
â HTML parsing PAUSED briefly for execution- Downloads in parallel with HTML parsing
- Executes as soon as downloaded (pauses parser during execution)
- No guaranteed order â scripts execute in download-completion order
- Best for: Independent scripts (analytics, ads) that don't depend on DOM or other scripts
defer
<script src="app.js" defer></script>HTML parsing: ââââââââââââââââââââââââââââââââ
Script fetch: ââââââââââââ
Script execute: ââââ
â After parsing, before DOMContentLoaded- Downloads in parallel with HTML parsing
- Executes after HTML is fully parsed, before
DOMContentLoaded - Preserves order â scripts execute in document order
- Best for: Application code that depends on the DOM
Comparison
| Attribute | Download | Execution | Order | DOM Available |
|---|---|---|---|---|
| (none) | Blocking | Blocking | Preserved | No (pauses parser) |
async | Parallel | On download complete | Not preserved | Maybe |
defer | Parallel | After parse, before DOMContentLoaded | Preserved | Yes |
type="module"
<script type="module" src="app.js"></script>Module scripts are deferred by default. They also:
- Execute in strict mode
- Have their own scope (no global pollution)
- Only execute once (even if included multiple times)
- Support
import/export
<!-- Module with async â executes as soon as ready, like async -->
<script type="module" src="widget.js" async></script>Page Lifecycle Events
Event Sequence
1. DOMContentLoaded â DOM tree built, scripts executed, stylesheets loaded
2. load â Everything loaded (images, iframes, stylesheets)
3. beforeunload â User attempting to leave (prompt to save)
4. unload â Page is being unloaded (cleanup)DOMContentLoaded
Fires when the HTML is fully parsed and the DOM tree is complete. Does NOT wait for images, stylesheets applied via <link>, or iframes:
document.addEventListener('DOMContentLoaded', () => {
// DOM is ready â safe to query elements
const app = document.getElementById('app');
initializeApp(app);
});Caveat: DOMContentLoaded does wait for:
<script>tags (non-async) to download and execute- Stylesheets that precede scripts (because scripts might query computed styles)
load
Fires when everything has loaded â images, iframes, stylesheets, subresources:
window.addEventListener('load', () => {
// Everything is loaded â safe to measure layout, initialize animations
hideLoadingSpinner();
measurePerformance();
});beforeunload
Fires when the user is about to leave. Use it to prompt for unsaved changes:
window.addEventListener('beforeunload', (e) => {
if (hasUnsavedChanges) {
e.preventDefault();
// Modern browsers ignore custom messages â show generic prompt
}
});Important: Don't use beforeunload for analytics â it delays navigation. Use navigator.sendBeacon() instead.
unload
Fires when the page is being unloaded. Extremely unreliable on mobile:
window.addEventListener('unload', () => {
// Don't rely on this â mobile browsers may kill the page without firing it
navigator.sendBeacon('/api/analytics', JSON.stringify(data));
});visibilitychange (Modern Alternative)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// Tab hidden â save state, pause video, send analytics
navigator.sendBeacon('/api/analytics', data);
} else {
// Tab visible â resume, refresh data
refreshData();
}
});visibilitychange is more reliable than unload/beforeunload on mobile and is the recommended approach for cleanup.
document.readyState
console.log(document.readyState);
// 'loading' â Document is still loading
// 'interactive' â DOM is parsed (DOMContentLoaded fires)
// 'complete' â All resources loaded (load fires)
document.addEventListener('readystatechange', () => {
console.log(document.readyState);
});Safe Initialization Pattern
function init() {
// Your initialization code
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init(); // DOMContentLoaded already fired
}Preloading & Prefetching
<!-- Preload: high priority, needed for current page -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/critical.css" as="style" />
<!-- Prefetch: low priority, needed for next navigation -->
<link rel="prefetch" href="/next-page.js" />
<!-- Preconnect: establish connection early -->
<link rel="preconnect" href="https://api.example.com" />
<!-- DNS Prefetch: resolve DNS early (lighter than preconnect) -->
<link rel="dns-prefetch" href="https://cdn.example.com" />| Hint | Priority | Use Case |
|---|---|---|
preload | High | Current page critical resources (fonts, hero image, main CSS) |
prefetch | Low | Resources for likely next navigation |
preconnect | Medium | Third-party origins you'll fetch from soon |
dns-prefetch | Low | Resolve DNS for third-party origins |
The Optimal Script Strategy
<!DOCTYPE html>
<html>
<head>
<!-- Critical CSS inlined -->
<style>/* critical styles */</style>
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
<!-- App scripts deferred (execute in order after parse) -->
<script src="/app.js" defer></script>
<!-- Analytics async (independent, no order dependency) -->
<script src="/analytics.js" async></script>
</head>
<body>
<!-- Content renders while scripts download -->
</body>
</html>Interview Signal
Senior candidates demonstrate:
- async vs defer â Download timing, execution timing, order guarantee, use cases for each
- DOMContentLoaded vs load â What each waits for, the stylesheet-blocks-script caveat
- Mobile lifecycle â
visibilitychangeoverunload,sendBeaconfor reliable analytics - Resource hints â
preloadvsprefetchvspreconnect, correctasattribute usage - Script placement â Why
deferin<head>is often better than scripts at end of<body>