Back

When to Run Your Code: Page Load Events Explained

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.

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.

OpenReplay