In a browser page, when does the document's DOMContentLoaded event fire, and how is that different from the window load event?
answer
- two finish lines, not one
- parsing done vs everything done
- images are not waited for
- deferred scripts run just before it
- readyState: loading, interactive, complete
basics
~20 sDOMContentLoaded fires as soon as the HTML has been fully parsed into a DOM and deferred scripts have run. It does not wait for images, iframes or stylesheets. The window load event fires later, once every subresource has finished loading.
solid answer
~40 s`DOMContentLoaded` is dispatched on `document` the moment the HTML parser reaches the end of the document and the DOM tree is complete, after any `defer` scripts have executed. It deliberately ignores subresources: images, iframes, stylesheets and `async` scripts may all still be in flight. `load` is dispatched on `window` afterwards, once the document *and* all of its dependent resources have finished loading (or failed). So the ordering is always DOMContentLoaded first, load second. In practice you attach initialisation code that only needs the DOM to `DOMContentLoaded`, and reserve `load` for work that genuinely needs final geometry or images, such as measuring a laid-out image. `document.readyState` tracks the same timeline: `"loading"`, then `"interactive"` around DOMContentLoaded, then `"complete"` just before `load`.
go deeper
Be able to state the order plainly: DOMContentLoaded when the HTML is parsed, load when images and other subresources are also done. Say which one you would use to wire up event handlers, and why.
Explain the mechanics behind the timing: deferred scripts run before DOMContentLoaded, async scripts are not waited for, and a parser-blocking script postpones it. Map both events onto document.readyState's interactive and complete states.
Show judgment about which signal a piece of work belongs on, and diagnose the failure mode where a late-injected script misses a one-shot event. Explain why load is a weak readiness signal on image-heavy pages.
Own the position that neither event is a good product-level readiness metric for a modern app, and be able to argue what the team should instrument instead, and what regressions a load-based measurement will hide or exaggerate.
## Two different finish lines A page load has two milestones that people routinely conflate. `DOMContentLoaded` means *the document has been parsed*. `load` means *the document and everything it referenced has arrived*. They can be milliseconds apart on a text page and many seconds apart on a page with large images. ## What has to be true before DOMContentLoaded The HTML parser consumes the response incrementally and appends nodes to the DOM as it goes. `DOMContentLoaded` is dispatched on `document` when the parser reaches the end of the document — the DOM tree is final, nothing more will be appended by the parser. Two things can push that moment later: - **A parser-blocking classic script.** The parser stops at the script, hands control to the JavaScript engine, and only resumes afterwards. The DOM cannot be complete until that finishes. - **`defer` scripts.** Deferred scripts are guaranteed to run after parsing completes and *before* `DOMContentLoaded`, in document order. So the event waits for them. `async` scripts are not waited for. An async script may execute before or after `DOMContentLoaded` depending purely on when its download finishes — that non-determinism is the whole point of `async`. ## What DOMContentLoaded does not wait for Images, `<iframe>` documents, videos, fonts and stylesheets are all *subresources*. Their loading is independent of parsing, so `DOMContentLoaded` fires without them. This is exactly why it is the right hook for most initialisation: your query selectors and event wiring only need nodes to exist. There is one indirect coupling worth knowing. A pending stylesheet does not block the parser from building the DOM, but it *does* block a following synchronous script from executing — the script might call `getComputedStyle()`, so the browser makes it wait for the CSSOM. Since the parser is in turn waiting on that script, a slow stylesheet can delay `DOMContentLoaded` transitively. The event still has no direct dependency on CSS. ## The load event `load` fires on `window` once the document is complete and every dependent resource has either loaded or failed. A 404 image does not hang `load` forever — a failed fetch also settles the resource. Because `load` waits for the slowest image on the page, it is a poor signal for "the app is ready" and a decent signal for "final layout with real image dimensions exists". ## document.readyState The same timeline is exposed as a string on `document`: ```js document.readyState; // "loading" while the parser is running // -> "interactive" when parsing is done (DOMContentLoaded fires just after) // -> "complete" when subresources are done (load fires just after) ``` A `readystatechange` event fires on each transition. ## The late-listener trap These are one-shot events, not sticky flags. If your script runs after the event has already been dispatched — a dynamically injected script, a bundle loaded very late — the listener never fires and your initialisation silently never happens. The standard guard is: ```js function init() { /* ... */ } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } ``` ## Where to attach what - DOM wiring, event delegation, hydration of markup: `DOMContentLoaded` (or simply a `defer`red script, which runs at effectively the same point and needs no listener at all). - Measuring an element whose size depends on a loaded image, or firing analytics that should reflect a fully drawn page: `load`. - Cleanup and "the user is leaving" work: neither of these — the page-lifecycle events exist for that. ## Historical note jQuery's `$(document).ready()` and `$(window).on('load')` map onto exactly these two events, which is where a lot of the confusion originated: plenty of code used `load` when it only ever needed `ready`, and paid for it with a visibly late initialisation on image-heavy pages.
- Does DOMContentLoaded wait for stylesheets to finish downloading?Not directly — it is about parsing, and CSS is a subresource. But a pending stylesheet blocks a following synchronous script from executing, and the parser is blocked on that script, so a slow stylesheet can delay DOMContentLoaded transitively. With no blocking script in the way, CSS has no effect on when it fires.
- Your initialisation code stopped running after you moved it into a dynamically injected script. Why?The injected script very likely evaluated after DOMContentLoaded had already been dispatched, and these events are one-shot rather than sticky, so the listener never fires. Guard with `document.readyState === 'loading'` and call the initialiser directly otherwise.
- Where do async scripts sit relative to DOMContentLoaded?Nowhere fixed. An async script executes as soon as its download completes, which may be before or after DOMContentLoaded, and the event does not wait for it. If ordering relative to the DOM matters, async is the wrong choice.
saying these in an interview costs you the question
- Says load fires before DOMContentLoaded
- Thinks DOMContentLoaded waits for all images
- Believes async scripts delay DOMContentLoaded
- Attaches the listener late and expects it to still fire
- Treats window.onload as the general app-ready hook