When to Run Your Code: Page Load Events Explained
When should your JavaScript actually run? It’s a question every frontend developer faces, whether you’re manipulating the DOM in vanilla JavaScript or managing component lifecycles in React. The answer depends on understanding the browser’s page load events and choosing the right hook for your specific needs.
Key Takeaways
- DOMContentLoaded fires when HTML parsing completes, while load waits for all resources
- Modern browsers use Page Visibility and Lifecycle APIs instead of unreliable unload events
- React and other frameworks handle timing through component lifecycle methods
- Check document.readyState to avoid missing events that already fired
Understanding the Classic Browser Lifecycle
The browser fires events at specific points during page loading. Knowing when each fires—and what’s available at that moment—determines where your code belongs.
DOMContentLoaded vs load: The Critical Distinction
DOMContentLoaded fires when the HTML is fully parsed and the DOM tree is built. External resources like images, stylesheets, and iframes are still loading. This is your earliest opportunity to safely query and manipulate DOM elements:
document.addEventListener('DOMContentLoaded', () => {
// DOM is ready, but images might still be loading
const button = document.querySelector('#submit');
button.addEventListener('click', handleSubmit);
});
The load event waits for everything—images, stylesheets, iframes, and other external resources. Use it when you need complete resource information:
window.addEventListener('load', () => {
// All resources loaded - image dimensions are available
const img = document.querySelector('#hero');
console.log(`Image size: ${img.naturalWidth}x${img.naturalHeight}`);
});
How Scripts Affect Timing
Regular <script> tags block DOMContentLoaded—the browser must execute them before continuing. However, scripts with defer execute after DOM parsing but before DOMContentLoaded fires. Scripts with async load in parallel and execute immediately when downloaded, potentially before or after DOMContentLoaded.
For element-specific resources, use individual load events:
const img = new Image();
img.addEventListener('load', () => console.log('Image ready'));
img.src = 'photo.jpg';
Modern Approaches: document.readyState and Beyond
Using document.readyState
Instead of hoping your event listener is attached in time, check the current state:
function initialize() {
// Your initialization code
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
// DOMContentLoaded already fired
initialize();
}
The three states are:
'loading'- Document is parsing'interactive'- Parsing complete, DOMContentLoaded about to fire'complete'- All resources loaded
jQuery’s $(document).ready() was essentially a cross-browser wrapper for this pattern. Today, native APIs handle this reliably across browsers.
Discover how at OpenReplay.com.
Page Visibility and Lifecycle API: The New Reality
Why beforeunload and unload Are Problematic
Modern browsers aggressively optimize performance through features like the back-forward cache (bfcache). Pages entering bfcache aren’t unloaded—they’re suspended. This means beforeunload and unload events don’t reliably fire when users navigate away. Additionally, browsers now limit or ignore custom messages in beforeunload dialogs to prevent abuse.
The Page Visibility API Alternative
Instead of unreliable unload events, use the Page Visibility API to respond when users switch tabs or minimize the browser:
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Page is hidden - pause expensive operations
pauseVideoPlayback();
throttleWebSocket();
} else {
// Page is visible again
resumeOperations();
}
});
Page Lifecycle States
The Page Lifecycle API extends this with states like frozen (tab suspended to save memory) and terminated:
document.addEventListener('freeze', () => {
// Save state - tab might be discarded
localStorage.setItem('appState', JSON.stringify(state));
});
document.addEventListener('resume', () => {
// Restore after being frozen
hydrateState();
});
React useEffect vs DOMContentLoaded
In React and other modern frameworks, you rarely use DOMContentLoaded directly. Component lifecycle methods handle initialization timing:
// React component
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs after component mounts and DOM updates
// Similar timing to DOMContentLoaded for this component
initializeThirdPartyLibrary();
return () => {
// Cleanup on unmount
cleanup();
};
}, []); // Empty dependency array = run once after mount
return <div>Component content</div>;
}
For Next.js or other SSR frameworks, code in useEffect runs only on the client after hydration—the framework handles the complexity of server vs. client execution timing.
Choosing the Right Hook
For vanilla JavaScript:
- DOM manipulation: Use DOMContentLoaded
- Resource-dependent code: Use window load event
- State persistence: Use Page Visibility API
- Checking current state: Use document.readyState
For SPAs and frameworks:
- Component initialization: Use framework lifecycle (useEffect, mounted, etc.)
- Route changes: Use router events
- Background/foreground: Still use Page Visibility API
Avoid these patterns:
- Don’t rely on beforeunload/unload for critical operations
- Don’t use DOMContentLoaded in React components
- Don’t assume scripts run in a fresh page load (consider bfcache)
Conclusion
JavaScript page load events have evolved beyond simple DOMContentLoaded and load handlers. While these classic events remain essential for vanilla JavaScript, modern development requires understanding Page Visibility, Lifecycle APIs, and framework-specific patterns. Choose your initialization strategy based on what resources you need and whether you’re working with a server-rendered page or building a full SPA. Most importantly, don’t rely on deprecated patterns like unload events—embrace the modern APIs built for contemporary web applications.
FAQs
Defer scripts execute in order after DOM parsing but before DOMContentLoaded. Async scripts execute immediately when downloaded, potentially interrupting parsing. Use defer for scripts that need DOM access and async for independent scripts like analytics.
While window.onload works, addEventListener is preferred because it allows multiple handlers and won't overwrite existing ones. The load event itself remains useful when you need all resources fully loaded before running code.
Use useEffect with an empty dependency array for client-side initialization. This runs after hydration completes. For server-side code, use framework-specific methods like getServerSideProps or getStaticProps instead of browser events.
Modern browsers cache pages and may not fire beforeunload when users navigate away. Use the Page Visibility API instead to detect when users leave, and persist critical data continuously rather than on exit.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.