cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Page & resource loading

Run JavaScript at the right moment in the page lifecycle: DOMContentLoaded, load, async and defer scripts, visibility changes, beforeunload, and resource load errors.

By the end you can
  • 01
    Pick the right momentChoose DOMContentLoaded, load, visibilitychange, pagehide, or a resource event for a real task.
  • 02
    Predict script timingExplain how normal, defer, async, and module scripts affect parsing and DOMContentLoaded.
  • 03
    Handle page exits safelyPause work when hidden, save quickly on pagehide, and use beforeunload only for unsaved changes.

The page has a timeline

Browser JavaScript is not only about what to run. A big part of professional front-end work is choosing when to run it. If code searches for a button before the browser has parsed that button, the search returns null. If code measures an image before the image loads, the size can be wrong. If code keeps polling while the tab is hidden, it wastes battery and network.

This lesson gives names to the important moments: the DOM becomes ready, resources finish, scripts join the queue, the page is hidden, and individual images either load or fail. We will use real sandboxed frames for startup timing so React does not manage the nodes we are poking, and a live page demo for visibility because that signal belongs to this actual tab.

Working definition

Page lifecycle code listens for the moment that matches the job: parsed DOM for wiring elements, loaded resources for measurements, visibility changes for pausing or quick saves, and resource events for individual files.

Real-life analogyMoving into a new house

Imagine moving into a house. Once the walls, rooms, and furniture are in place, you can walk around and start arranging things. That is like DOMContentLoaded: the document structure exists. Later, every delivery truck has arrived too: pictures, rugs, and the big sofa. That is like load: external resources are done.

In real life: Walls and furniture are in place
In JavaScript: The HTML is parsed and DOMContentLoaded can run
In real life: Every delivery has arrived too
In JavaScript: load has fired after images, iframes, stylesheets, and other resources
In real life: Start arranging bookshelves
In JavaScript: Attach listeners to existing DOM at DOMContentLoaded or from a deferred script
In real life: Measure the sofa after it arrives
In JavaScript: Read final image/resource-dependent sizes at load or the resource’s own load

Where the analogy stops: A house has one moving day. A page can become hidden, enter the back/forward cache, return later, and load individual resources at different times.

If you want one rule to carry through the whole lesson, use this: choose the earliest event that has the facts you need. Earlier usually feels faster; later is only better when the job truly depends on later resources.

DOMContentLoaded, load, and readyState

INTERACTIVE

document.readyState moves through three values. It starts as loading while the parser is reading HTML. It becomes interactive when parsing is done, just before DOMContentLoaded. It becomes complete when the page’s load event is firing, after resources such as images and iframes are finished.

Startup moments
MomentWhat is readyDoes it wait for images?
loadingThe parser is still building the documentNo
interactiveHTML is parsed; deferred and module scripts run before DOMContentLoadedNo
DOMContentLoadedThe DOM is ready and deferred/module scripts finishedNo
complete / loadThe document and subresources are loadedYes, for ordinary page resources

A common safe pattern is: if the document is still loading, add a DOMContentLoaded listener; otherwise run immediately. That second branch matters because adding a listener after the event already fired will not replay it for you.

Step through a safe startup guard
Step 0 of 5Ready
Your turn: follow the blue line

Step through a safe helper for code that needs the parsed DOM. Change the readyState setting and notice why late listeners need a guard.

Running in
  1. script
Next: line 9
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function onDomReady(callback) {  if (document.readyState === "loading") {    document.addEventListener("DOMContentLoaded", callback, { once: true });  } else {    callback();  }} 
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Current document.readyState
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Now watch a real document go through its startup events. The code runs inside a sandboxed srcdoc iframe with sandbox="allow-scripts". It posts messages out, and the lesson accepts only messages whose event.source is that exact frame.

Loading timeline in a sandboxed frame
Code running inside the framePop out in the code editor (opens in a new tab)JavaScript
const started = performance.now();const log = (label) => parent.postMessage({ type: "timeline", label, ms: Math.round(performance.now() - started), state: document.readyState }, "*");log("script start");document.addEventListener("readystatechange", () => log("readystatechange"));document.addEventListener("DOMContentLoaded", () => log("DOMContentLoaded"));window.addEventListener("load", () => log("window load"));const img = document.querySelector("img");img.addEventListener("load", () => log("image load"));img.addEventListener("error", () => log("image error"));
Real browser timelinesuccess resource
    Try it yourself

    The frame posts timestamped events to the parent page. The parent accepts only messages whose source is this iframe. With a data URL we can prove success and error, but not a realistic slow network download without a server.

    The sandbox uses allow-scripts and srcdoc; no learner code is evaluated.

    In this server-free demo a data URL image can succeed and a deliberately broken data URL can fail. A generated data URL does not simulate a real slow network very well, so the experiment is honest about what it can prove: event order, readyState changes, and resource success/error.

    Normal, defer, async, and module scripts

    INTERACTIVE

    Classic script tags affect parsing differently depending on their attributes. A normal external script is like a guest who stops the builders until they finish talking. A defer script waits politely until the house is built, then speaks in document order before DOMContentLoaded. An async script interrupts whenever it arrives, in whatever order downloads finish.

    Real-life analogyThree kinds of script guests

    The HTML parser is trying to build the page. Some script guests block the doorway, some wait their turn, and some interrupt as soon as they arrive. Your job is to invite each script in the mode that matches what it depends on.

    In real life: A guest blocks the hallway to tell a long story
    In JavaScript: A normal script blocks parsing while it downloads and runs
    In real life: Guests wait politely and speak in the invitation order
    In JavaScript: defer scripts run after parsing, in document order
    In real life: A courier bursts in whenever the package arrives
    In JavaScript: async scripts run as soon as ready, order not guaranteed
    In real life: A scheduled speaker already knows to wait
    In JavaScript: Module scripts are deferred by default unless marked async

    Where the analogy stops: Browsers also optimize fetching and stylesheets can delay scripts that come after them. The analogy explains ordering, not every network optimization.

    Script order lab
    Shape of the frameHTML
    log("parser before script");<script src="data:text/javascript,..."></script>log("parser after script");document.addEventListener("DOMContentLoaded", () => log("DOMContentLoaded"));
    Execution orderdefer
      Try it yourself

      External data: scripts run in the sandboxed frame. Defer preserves order before DOMContentLoaded; async runs whenever downloads finish, so quick data URLs may look stable but the guarantee is intentionally weaker.

      async and defer affect external classic scripts. They do nothing for inline classic scripts; module scripts are deferred by default.

      Important details: defer and async apply to external classic scripts with src; they do not change inline classic scripts. Module scripts behave like defer by default, and can be made async. Also remember that a stylesheet can delay a script that comes after it, because the script might ask for computed styles.

      Why earlier async lessons matter

      Script loading is separate from the event loop, but they meet once a script actually runs. If the task/microtask vocabulary feels fuzzy, review The event loop.

      visibilitychange, pagehide, and beforeunload

      LIVE PAGE

      A page does not always get a clean goodbye. Mobile browsers may freeze it. The user may put it into the back/forward cache and return without a full reload. That is why unload is a bad place for important work: it is unreliable and can block the back/forward cache.

      Use visibilitychange when the page becomes hidden or visible. It is like someone leaving the room and turning off the TV: pause animations, stop polling, and send tiny final messages early. Use pagehide for page navigation signals, especially with event.persisted to notice bfcache. Use beforeunload only for the unsaved-changes prompt.

      Real-life analogyLeaving the room

      When nobody is watching the TV, you pause it. Web pages should do the same with work the user cannot see. Save quickly, then get out of the browser’s way.

      In real life: Someone leaves the room
      In JavaScript: The page becomes hidden
      In real life: Turn off the TV and stop the game clock
      In JavaScript: Pause animation, polling, and expensive timers
      In real life: They might come right back
      In JavaScript: The page may enter bfcache and later fire pageshow

      Where the analogy stops: A hidden page may still run briefly, but browsers can throttle, freeze, or discard background pages. Keep hidden-work small and resilient.

      Visibility and unsaved-changes lab
      Visibility and beforeunload patternsPop out in the code editor (opens in a new tab)JavaScript
      document.addEventListener("visibilitychange", () => {  if (document.visibilityState === "hidden") {    pauseAnimation();    navigator.sendBeacon?.("/analytics", draftSnapshot);  } else {    resumeAnimation();  }}); window.addEventListener("pagehide", (event) => {  saveFast("leaving or entering bfcache", event.persisted);}); function warnIfDirty(event) {  event.preventDefault();  event.returnValue = ""; // legacy signal; browsers ignore custom text} function setDirty(isDirty) {  if (isDirty) {    window.addEventListener("beforeunload", warnIfDirty);  } else {    window.removeEventListener("beforeunload", warnIfDirty);  }}
      Live page signalsunknown
      visible animation ticks0
        Try it yourself

        Switch tabs or minimize the browser. The log updates on visibilitychange, and the animation counter only advances while visible.

        Reset removes the beforeunload listener. The demo never uses unload.

        The beforeunload pattern is deliberately narrow: event.preventDefault() plus the legacy event.returnValue = "". Modern browsers show their own generic message, ignore custom text, and in most cases require the user to have interacted with the page first. Register it only while changes are actually unsaved, and remove it as soon as the page is clean.

        Unsaved changes patternPop out in the code editor (opens in a new tab)JavaScript
        function warnIfDirty(event) {  event.preventDefault();  event.returnValue = ""; // legacy signal; browsers ignore custom text} function setDirty(isDirty) {  if (isDirty) {    window.addEventListener("beforeunload", warnIfDirty);  } else {    window.removeEventListener("beforeunload", warnIfDirty);  }}

        onload and onerror for resources

        INTERACTIVE

        The document has loading events, and so do many resource elements. Images, scripts, stylesheets, iframes, and other resources can fire load when they succeed and error when they fail. You can attach handlers with properties such as img.onload, but addEventListener is usually easier to compose and remove.

        Image load/error promise wrapper
        Promisifying image loadingPop out in the code editor (opens in a new tab)JavaScript
        function loadImage(src) {  return new Promise((resolve, reject) => {    const img = new Image();    img.addEventListener("load", () => resolve(img));    img.addEventListener("error", () => reject(new Error("Image failed")));    img.src = src;  });}
        Resultreal Image object

        Choose a resource to load.

        Try it yourself

        Choose a resource to load.

        The same pattern works for other resource elements with load and error events.

        Wrapping resource loading in a Promise is a practical form of promisification: translate callback-style events into a value you can await. If that idea is new, the Promisification lesson explains the general pattern.

        Promise wrapper for one imagePop out in the code editor (opens in a new tab)JavaScript
        function loadImage(src) {  return new Promise((resolve, reject) => {    const img = new Image();    img.addEventListener("load", () => resolve(img));    img.addEventListener("error", () => reject(new Error("Image failed")));    img.src = src;  });}

        Where you will use this

        SORT IT

        Lifecycle choices show up in ordinary product code: start menus as soon as the DOM exists, wait for a chart image before measuring, pause a dashboard when hidden, and save small data before the browser freezes the page. The sorter below asks for the earliest useful moment.

        Which moment?
        • Attach click listeners to buttons that already exist in the HTML
        • Start a menu script that reads the parsed navigation
        • Read the final size of a hero image after it loads
        • Wait until an embedded iframe has loaded its document
        • Save a draft when the user switches tabs
        • Send a small analytics beacon as the page is hidden or put in the bfcache
        • Warn before leaving while a form has unsaved changes
        • Remove the leave warning after the draft is saved
        Try it yourself
        0 of 8 correct

        Put each task on the lifecycle moment that best fits it.

        Choose a category for every card. You can change an answer at any time; Reset clears them all.
        A realistic startup skeletonPop out in the code editor (opens in a new tab)JavaScript
        function startWhenReady(setup) {  if (document.readyState === "loading") {    document.addEventListener("DOMContentLoaded", setup, { once: true });  } else {    setup();  }} startWhenReady(() => {  document.querySelector("#save").addEventListener("click", saveDraft);}); document.addEventListener("visibilitychange", () => {  if (document.visibilityState === "hidden") navigator.sendBeacon("/draft", currentDraft());});

        Common misconceptions

        • “DOMContentLoaded means everything loaded.” It means the parsed DOM is ready and deferred/module scripts finished. Images and iframes can still be loading.
        • “load is always better because it is safer.” It is later. Use it only when you need resources, not just elements.
        • “async and defer work on inline scripts.” They matter for external classic scripts. Inline classic scripts run where they appear.
        • “async scripts run after DOMContentLoaded.” They may run before or after it; the point is that download completion controls them.
        • “beforeunload lets me write a custom warning.” Modern browsers show generic text, and the prompt is restricted.
        • “unload is the right place for cleanup.” It is unreliable and harms bfcache. Prefer visibilitychange, pagehide, and sendBeacon for small work.
        Similar lifecycle tools
        ToolBest forAvoid using it for
        DOMContentLoadedDOM exists; attach listenersWaiting for images or final layout
        loadAll page subresources loadedBasic startup that could happen earlier
        visibilitychangePause, resume, quick save when hiddenBlocking navigation with a prompt
        pagehideNavigation away, including bfcache checksLong synchronous cleanup
        beforeunloadUnsaved-changes prompt onlyAnalytics, custom messages, or routine cleanup

        Practice exercises

        5 EXERCISES
        Exercise 1 · Warm-upPredict the ready guard

        What does the program print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const documentModel = { readyState: "interactive", addEventListener() { console.log("listener added"); } };
        function onReady(callback) {
          if (documentModel.readyState === "loading") {
            documentModel.addEventListener("DOMContentLoaded", callback);
          } else {
            callback();
          }
        }
        onReady(() => console.log("run setup"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 2 · Warm-upChoose the leaving signal

          Type the event name returned by the program.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          function momentFor(task) {
            if (task.includes("image size")) return "load";
            if (task.includes("switches tabs")) return "visibilitychange";
            return "DOMContentLoaded";
          }
          console.log(momentFor("save when user switches tabs"));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 3 · PracticeFind the script attribute

            Which attribute keeps external classic scripts ordered and runs them before DOMContentLoaded?

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 4 · PracticeBroken image handler

              Which resource event handles a broken image?

              Answer, then press Check. Spacing and letter case don’t matter.

                Exercise 5 · ChallengeRemove the risky cleanup

                A teammate put analytics in window.addEventListener("unload", ...). Which event name should you tell them to avoid?

                Answer, then press Check. Spacing and letter case don’t matter.

                  Check your understanding

                  8 QUESTIONS
                  Page lifecycle quiz · 8 questionsScore: first tries count
                  1. Question 1 of 8What does DOMContentLoaded wait for?

                    Choose an answer to see the explanation.

                  2. Question 2 of 8What does this print in the model?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    const documentModel = { readyState: "interactive", addEventListener() { console.log("listener added"); } };
                    function onReady(callback) {
                      if (documentModel.readyState === "loading") documentModel.addEventListener("DOMContentLoaded", callback);
                      else callback();
                    }
                    onReady(() => console.log("run setup"));

                    Choose an answer to see the explanation.

                  3. Question 3 of 8Which script attribute executes external classic scripts in document order after parsing and before DOMContentLoaded?

                    Choose an answer to see the explanation.

                  4. Question 4 of 8What is true about inline scripts?

                    Choose an answer to see the explanation.

                  5. Question 5 of 8Which signal is best for pausing polling or animation when the user switches tabs?

                    Choose an answer to see the explanation.

                  6. Question 6 of 8How do modern browsers usually show a beforeunload warning?

                    Choose an answer to see the explanation.

                  7. Question 7 of 8Which image event fires for a broken data URL?

                    Choose an answer to see the explanation.

                  8. Question 8 of 8Why avoid unload for saving analytics?

                    Choose an answer to see the explanation.

                  Key takeaways

                  • DOMContentLoaded is for parsed DOM; load is for resources too.
                  • Check document.readyState before adding a startup listener late.
                  • defer keeps external classic scripts ordered before DOMContentLoaded; async does not.
                  • Pause and save on visibilitychange/pagehide; avoid unload.
                  • Use resource load/error events for individual images and files.

                  Page lifecycle work means matching each piece of JavaScript to the earliest browser moment where the facts it needs are true.

                  Up next: Form properties & elements.

                  CompleteFrontend Clear concepts. Working examples.