Domcontentloaded And Modules
DOMContentLoaded fires before the user sees anything. load waits for everything. Knowing which to use, and how modules change variable scoping, shapes how you structure a vanilla JS app.
With defer on the script tag, the code runs after HTML parsing finishes. But there is still a distinction worth making between parsing being done and the DOM being ready for safe manipulation. And once we add type="module" to organize our code, the scoping model changes entirely.
The Essentials
- DOMContentLoaded: Fires when the HTML is fully parsed and the DOM tree is built. Images, stylesheets, and fonts may still be loading. This is the right place to initialize your app.
- load event: Fires when everything on the page has fully loaded, including images, web fonts, and embedded media. Too late for most initialization work.
- type="module": Converts your entry-point script into an ES module. Variables inside a module are file-scoped, not global. Enables
importandexportbetween files. - window.app pattern: A deliberate global namespace for app state that needs to be accessible across modules. Better than polluting the global scope with many individual variables.
Two Events, Two Different Moments
When the browser loads a page, there is not one moment when "everything is ready" -- there are two moments worth distinguishing.
The first moment is when the HTML parser finishes and the DOM tree is fully built in memory. That is DOMContentLoaded. At this point you can query for any element in the document and it will be there. Images may still be loading. Fonts may still be rendering. Videos may still be buffering. None of that matters for initializing app behavior.
window.addEventListener('DOMContentLoaded', () => {
// DOM is built. Safe to query elements and attach event listeners.
const nav = document.querySelector('nav');
nav.addEventListener('click', handleNavClick);
});The second moment is when every resource the page references has fully loaded. That is the load event. Waiting for this before initializing means your JavaScript is not wired up while the user is already looking at the page and trying to interact with it.
window.addEventListener('load', () => {
// Everything is loaded -- but this might be seconds after the user
// has already started clicking on things
});Almost all app initialization code belongs in DOMContentLoaded, not load.
Note: even with defer, using DOMContentLoaded is still good practice. defer guarantees your script runs after parsing, but the spec leaves a narrow window where some browsers may still be finishing the DOM object construction. DOMContentLoaded closes that gap.
From Scripts to Modules
Without modules, every JavaScript file loaded on a page shares the same global scope. Any variable declared at the top level of one file is accessible from any other file. This means variable name collisions are a real risk, and there is no way to see which file "owns" a given variable.
Adding type="module" to the entry-point script tag changes this:
<script type="module" src="app.js"></script>Now app.js is an ES module. Every variable declared at the top level of app.js stays in app.js. Other files cannot see them unless app.js explicitly exports them. And app.js can import from other files using import.
This is how modern JavaScript organizes code into separate files without needing a bundler.
The window.app Pattern
Modules scope variables to their files, which means you can no longer use global variables to share state across files the way classic scripts could. This is mostly a good thing. But sometimes you genuinely need state to be accessible anywhere in the app.
One clean approach is to create a single intentional global namespace:
// In app.js
import Store from './services/Store.js';
import Router from './services/Router.js';
window.app = {};
window.app.store = Store;
window.app.router = Router;Now any module that needs the store can access it through window.app.store. There is one deliberate global object instead of many scattered globals. If a future browser API uses the name app, you can prefix it to avoid a collision.
This pattern keeps the benefits of modules (file-scoped variables, explicit imports) while still allowing a controlled amount of shared state at the application level.
Further Reading and Watching
- MDN: DOMContentLoaded - Event reference including timing relative to
loadanddefer. - MDN: JavaScript modules - Full guide to
import,export, default exports, and named exports.
Video:
- JavaScript Modules (ES6) by Traversy Media. Walks through
import/exportsyntax with practical examples.