Back to Browser Internals
Browser Internals
medium
mid

What events fire while a website is loading, and how do they map to the Critical Rendering Path?

Load lifecycle events: DOMContentLoaded (DOM parsed, before images/stylesheets finish), load (everything including subresources done), beforeunload/unload/pagehide (leaving), readystatechange. Plus the modern way: Performance API / PerformanceObserver and Core Web Vitals (LCP, FCP, CLS) for real measurement.

4 min read·~6 min to think through

There are the classic lifecycle events and the modern measurement APIs — a strong answer covers both.

The classic load lifecycle events

  • DOMContentLoaded — fires when the HTML is fully parsed and the DOM is built — but before images, stylesheets, and subframes necessarily finish loading. This is when it's safe to query/manipulate the DOM. (Deferred scripts run just before this.) It's the event you usually want for "start my app."
  • load (on window) — fires when everything is done: the DOM plus all subresources — images, CSS, fonts, iframes. Later than DOMContentLoaded. Use it when you genuinely need all assets (e.g. measuring final layout).
  • readystatechange + document.readyStateloadinginteractive (≈ DOMContentLoaded) → complete (≈ load).
  • Leaving the page: beforeunload (can prompt "unsaved changes"), pagehide, unload (unload is unreliable — pagehide/visibilitychange are preferred for cleanup/beacons).
  • visibilitychange — tab backgrounded/foregrounded; the modern, reliable hook for "user is leaving" (send analytics beacons here).

The modern way: actually measure the CRP

Lifecycle events tell you when phases finish; the Performance APIs tell you how the CRP performed:

  • performance.timing / Navigation Timing / performance.getEntriesByType("navigation") — precise timestamps for DNS, TCP, TLS, request, response, DOM processing, etc.
  • PerformanceObserver — subscribe to performance entries as they happen.
  • Paint Timingfirst-paint and First Contentful Paint (FCP).
  • Core Web Vitals, observed via PerformanceObserver:
  • LCP (Largest Contentful Paint) — when the main content rendered.
  • CLS (Cumulative Layout Shift) — visual stability.
  • INP (Interaction to Next Paint) — responsiveness.
  • Resource Timing — per-asset load timings.

Tying it to the CRP

The CRP is HTML→DOM→CSSOM→render tree→layout→paint→composite. The events map onto it: DOMContentLoaded ≈ "DOM built," FCP ≈ "first paint happened," LCP ≈ "main content painted," load ≈ "all subresources done." To optimize the CRP you measure with these APIs, then reduce render-blocking CSS, defer JS, etc.

The framing

"Two layers. Classic lifecycle events: DOMContentLoaded when the DOM is parsed — before images/CSS finish — which is when it's safe to touch the DOM and usually when you start your app; load when everything including subresources is done; readystatechange, and pagehide/visibilitychange for cleanup and beacons since unload is unreliable. But the modern, more useful layer is measurement: the Performance API and PerformanceObserver give you Navigation Timing, FCP, and Core Web Vitals — LCP, CLS, INP — which is how you actually quantify and then optimize the Critical Rendering Path."

Follow-up questions

  • What's the difference between DOMContentLoaded and load?
  • Why is the unload event unreliable, and what do you use instead?
  • How do you measure FCP and LCP programmatically?
  • How do the load events map onto the stages of the CRP?

Common mistakes

  • Confusing DOMContentLoaded (DOM ready) with load (everything ready).
  • Relying on the unload event for cleanup/analytics.
  • Only naming classic events, missing the Performance API entirely.
  • Not connecting the events to the CRP stages.

Performance considerations

  • The Performance APIs are how you instrument real-user monitoring (RUM) of the CRP — FCP, LCP, CLS, INP, navigation timing. Listening on the right events (visibilitychange over unload) ensures beacons actually send.

Edge cases

  • Deferred/async scripts and their timing relative to DOMContentLoaded.
  • bfcache (back-forward cache) and pageshow/pagehide.
  • Slow subresources delaying load long after the page is usable.
  • SPA route changes — none of these fire; you measure differently.

Real-world examples

  • web-vitals library reporting LCP/CLS/INP via PerformanceObserver to analytics.
  • Sending an analytics beacon on visibilitychange instead of unload.

Senior engineer discussion

Seniors cover the classic events accurately (DOMContentLoaded vs load, unload's unreliability), then pivot to the Performance API and Core Web Vitals as the real way to observe and optimize the CRP, mapping events onto CRP stages.

Related questions