DNA🌐 Web BrowserScript Loading & Page Lifecycle Events
ðŸĢHatchlingBrowserPerformanceScript LoadingLifecycle

Script Loading & Page Lifecycle Events

How and when scripts load determines your page's interactivity time. Understanding async, defer, DOMContentLoaded, and the load event sequence is fundamental to frontend performance.

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 PAUSED

The 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

AttributeDownloadExecutionOrderDOM Available
(none)BlockingBlockingPreservedNo (pauses parser)
asyncParallelOn download completeNot preservedMaybe
deferParallelAfter parse, before DOMContentLoadedPreservedYes

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" />
HintPriorityUse Case
preloadHighCurrent page critical resources (fonts, hero image, main CSS)
prefetchLowResources for likely next navigation
preconnectMediumThird-party origins you'll fetch from soon
dns-prefetchLowResolve 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:

  1. async vs defer — Download timing, execution timing, order guarantee, use cases for each
  2. DOMContentLoaded vs load — What each waits for, the stylesheet-blocks-script caveat
  3. Mobile lifecycle — visibilitychange over unload, sendBeacon for reliable analytics
  4. Resource hints — preload vs prefetch vs preconnect, correct as attribute usage
  5. Script placement — Why defer in <head> is often better than scripts at end of <body>