completefrontend
Open lab My progress
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 single file's own event for a real task.
  • 02
    Predict when scripts runExplain how normal, defer, async, and module scripts change the reading of the HTML and the timing of DOMContentLoaded.
  • 03
    Handle leaving the page safelyPause work when the page is hidden, save quickly on pagehide, and use beforeunload only for unsaved changes.

The page has a timeline

A web page does not appear all at once. The browser reads the HTML from top to bottom and builds the page. Then it downloads images and other files. Your JavaScript must run at the right moment in that timeline.

Code that runs too early or too late causes real bugs:

  • Code looks for a button before the browser has read that button. It gets null, which means "nothing found".
  • Code measures an image before the image arrives. It gets the wrong size.
  • Code keeps asking the server for news while the tab is hidden. It wastes battery and data.

This lesson names the important moments. Each moment is an event: a signal from the browser that your code can listen for. You will meet these moments:

  • The HTML is fully read, and the DOM is ready. The DOM is the tree of elements the browser builds from your HTML.
  • Every image and other file has finished loading.
  • Scripts run, early or late, depending on how you add them.
  • The page is hidden, or the user leaves it.
  • One single image loads, or fails.

Some demos run a real page inside a small closed-off frame on this page, so you can watch it start up. The hidden-page demo uses this real tab, because "hidden" is about the tab you are reading.

Working definition

Page lifecycle code runs at the moment that fits its job:

  • Add listeners to elements once the HTML is read.
  • Measure things once their images have loaded.
  • Pause work or save quickly when the page is hidden.
  • Watch one file with that file's own events.
Real-life analogyMoving into a new house

Imagine moving into a new house. Once the walls and furniture are in place, you can walk around and start filling the shelves. That is like DOMContentLoaded: the page's structure exists.

Later, every delivery truck has arrived too, with the pictures, the rugs, and the big sofa. That is like load: all the extra files are done.

In real life: The walls and furniture are in place
In JavaScript: The HTML is read, and DOMContentLoaded fires
In real life: Every delivery has arrived too
In JavaScript: load fires after images, iframes, stylesheets, and other files
In real life: You start filling the bookshelves
In JavaScript: You add listeners to elements at DOMContentLoaded, or from a defer script
In real life: You measure the sofa after it arrives
In JavaScript: You read an image's final size at load, or at that image's own load

One rule runs through the whole lesson: use the earliest event that already has what your code needs. An earlier event makes the page feel faster. Wait for a later event only when your code needs something that arrives later.

DOMContentLoaded, load, and readyState

INTERACTIVE

document.readyState is a word that says how far the page has got. It has three values, always in this order:

  • loading: the browser is still reading the HTML.
  • interactive: the HTML is fully read. DOMContentLoaded fires soon after, once any defer scripts have run. You will meet those in the next section.
  • complete: images, iframes, and other files are done. The load event fires now.

Read each row of this table from left to right: the moment, what is ready, and whether it waits for images.

Startup moments: what is ready at each one
The momentWhat is readyDoes it wait for images?
loadingThe browser is still reading the HTML.No.
interactiveThe HTML is fully read. defer and module scripts run next.No.
DOMContentLoadedEvery element exists, and defer and module scripts have finished.No.
complete / loadThe page and all its files have loaded.Yes, for normal images, iframes, and stylesheets.
The three readyState values in orderPop out in the code editor (opens in a new tab)JavaScript
const states = ["loading", "interactive", "complete"];
console.log(states.join(" -> "));

Line 1 lists the three values in order. Line 2 joins them with arrows, so it prints loading -> interactive -> complete. These are steps the page goes through. You do not need to keep checking them in a loop.

document.readyState goes from loading, to interactive, to complete. Loading: the HTML is being read. Interactive: the HTML is read, and DOMContentLoaded fires. Complete: the files are done, and load fires.document.readyStateloadinginteractivecompletereading HTMLHTML is readDOMContentLoadedfiles are doneload
readyState only moves forward. DOMContentLoaded comes with interactive, and load comes with complete.
readystatechange can fire more than oncePop out in the code editor (opens in a new tab)JavaScript
const changes = ["interactive", "complete"];
for (const state of changes) {
  console.log("readystatechange: " + state);
}

readystatechange is an event that fires each time readyState changes. This loop copies that: it prints one line for each change. So a readystatechange listener can run more than once. If you want one clear starting point, listen for the event made for it, such as DOMContentLoaded.

A safe startup helper has two branches:

  • If readyState is still loading, add a DOMContentLoaded listener.
  • Otherwise, run the code right away.

The second branch matters. If you add a listener after DOMContentLoaded has already fired, the browser does not fire it again. Your listener would never run.

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

Step through a safe helper for code that needs the page's elements. Change the readyState setting to see why a late listener needs this check.

Running in
  1. script
Next: line 9
function onDomReady(callback) {  if (document.readyState === "loading") {    document.addEventListener("DOMContentLoaded", callback, { once: true });  } else {    callback();  }} 
CallStoreChangeResultBlue line: runs next. 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 page start up. Its code runs inside an iframe: a small page shown inside this page. Three things keep it safe and separate:

  • Its HTML comes from the srcdoc attribute, not from a server.
  • sandbox="allow-scripts" lets it run scripts but cuts it off from this page.
  • It sends each event out with postMessage. This page accepts a message only when event.source is that exact frame.
Watch a real page start up
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"));
Events in ordergood image
    Try it yourself

    The frame sends each event, with its time, to this page. This page accepts only messages that come from this frame. A data URL can show an image that works and one that fails, but not a slow download.

    The frame runs only the code shown here, with sandbox="allow-scripts" and srcdoc. It does not run any code you type.

    The images here are data URLs: the image data is written inside the address itself, so no server is needed. One image is good, and one is broken on purpose. A data URL arrives almost at once, so this demo cannot copy a slow network. It does show three real things:

    • the order of the events,
    • how readyState changes,
    • whether the image loaded or failed.

    Normal, defer, async, and module scripts

    INTERACTIVE

    A script tag can change how the browser reads the page. It depends on the tag's attributes, the extra words inside the tag such as defer. These rules are about external scripts: scripts loaded from a file with src.

    • A normal external script stops the browser from reading the HTML. The browser downloads the script, runs it, and only then reads on.
    • A defer script downloads while the browser keeps reading. It runs after all the HTML is read, in page order, before DOMContentLoaded.
    • An async script also downloads while the browser keeps reading. It runs as soon as it arrives, in whatever order the downloads finish.
    Real-life analogyThree kinds of script guests

    Builders are putting up a house from a plan. Guests arrive while they work:

    • Some guests block the hallway until they finish talking.
    • Some guests wait politely for their turn.
    • Some guests burst in the moment they arrive.

    You choose how each guest behaves, based on what it needs.

    In real life: A guest stands in the hallway and tells a long story
    In JavaScript: A normal script stops the HTML reading while it downloads and runs
    In real life: Guests wait politely and speak in the order on the invitation
    In JavaScript: defer scripts run after the HTML is read, in page order
    In real life: A courier bursts in as soon as the package arrives
    In JavaScript: async scripts run as soon as they download, in any order
    In real life: A booked speaker already knows to wait
    In JavaScript: Module scripts (type="module") wait like defer, unless marked async
    Pick defer or asyncPop out in the code editor (opens in a new tab)JavaScript
    function chooseScriptMode(needsOrder) {
      return needsOrder ? "defer" : "async";
    }
    
    console.log(chooseScriptMode(true));
    console.log(chooseScriptMode(false));

    The first call says the script needs order, so it prints defer. The second call says the script works on its own, so it prints async. Choose by what the script needs, not by which word sounds faster.

    Left: two defer scripts, A then B, run in page order, then DOMContentLoaded fires. Right: two async scripts. Script B finished downloading first, so it runs before script A.deferscript Ascript BDOMContentLoadedpage order keptasyncscript Bscript Awhichever downloadsfirst runs firstpage order not kept
    defer scripts run in the order they appear in the page, then DOMContentLoaded fires. async scripts run as soon as each one downloads, so B can run before A.
    Watch scripts run in order
    The frame's HTML, shortenedHTML
    log("parser before script");<script src="data:text/javascript,..."></script>log("parser after script");document.addEventListener("DOMContentLoaded", () => log("DOMContentLoaded"));
    The order they randefer
      Try it yourself

      Each script is a tiny data URL that runs in the frame. defer keeps page order and runs before DOMContentLoaded. async runs each script when its download finishes. These files arrive almost at once, so async may look ordered here, but that order is not promised.

      async and defer change external classic scripts only. They do nothing for inline classic scripts. Module scripts wait like defer by default.

      A few more details to remember:

      • defer and async only change external classic scripts. A classic script is a normal script, not a module. An inline script, written straight inside the tag, runs right where it appears.
      • Module scripts (type="module") wait like defer by default. You can add async to them too.
      • A stylesheet can delay a script that comes after it. The script might ask for the page's styles, so the browser waits for the stylesheet first.
      How this links to the event loop

      Script loading decides when a script starts. Once the script runs, the event loop decides the order of its callbacks. If tasks and microtasks feel unclear, review The event loop.

      visibilitychange, pagehide, and beforeunload

      LIVE PAGE

      A page does not always get a clean goodbye:

      • A phone browser may freeze a page that is in the background.
      • When the user presses Back, the browser may bring the page back from the back/forward cache, or bfcache. This cache keeps a whole page in memory, so Back is instant and nothing reloads.

      So unload, the event for a page closing, is a bad place for important work. It often does not fire. Using it can also stop the bfcache from working.

      visibilitychange says the page was hidden or shown. It does not promise that the page is closing. A hidden page may come back, freeze, or be thrown away. So save a small copy that is safe to save more than once. Do not count on one last callback.

      Use each event for its own job:

      • visibilitychange: the page became hidden or visible. Pause animations, stop asking the server for updates, and send small final messages early.
      • pagehide: the user is leaving the page. Its event.persisted is true when the page is going into the bfcache.
      • beforeunload: only for the "you have unsaved changes" warning.
      Real-life analogyLeaving the room

      When nobody is watching the TV, you pause it. A web page should do the same with work the user cannot see. Save quickly, then let the browser rest.

      In real life: Someone leaves the room
      In JavaScript: The page becomes hidden
      In real life: You turn off the TV and stop the game clock
      In JavaScript: You pause animations, server checks, and costly timers
      In real life: They might come right back
      In JavaScript: The page may go into the bfcache and fire pageshow when it comes back
      Pause when hidden, resume when visiblePop out in the code editor (opens in a new tab)JavaScript
      function workFor(state) {
        return state === "hidden" ? "pause and save" : "resume";
      }
      
      console.log(workFor("hidden"));
      console.log(workFor("visible"));

      The first call prints pause and save because the page is hidden. The second prints resume because the page is visible again. In a real listener, document.visibilityState gives you the state: "hidden" or "visible".

      When the page is hidden, the code pauses and saves. When the page is visible again, the code resumes.hiddenyou left the tabpause and savevisibleyou came backresume
      Hidden means the user cannot see the page, for example after switching tabs. Pause work and save; resume when the page is visible again.
      Hidden page and unsaved-changes lab
      Pause when hidden, warn while unsavedPop 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 eventsunknown
      counter (goes up while visible)0
        Try it yourself

        Switch to another tab, then come back. The log adds a line on each visibilitychange. The counter only goes up while the page is visible.

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

        Keep the beforeunload code small. It needs two lines:

        • event.preventDefault() asks the browser to show its warning.
        • event.returnValue = "" asks the same thing in older browsers.

        Modern browsers show their own general message and ignore any text you write. Most also show it only if the user has clicked or typed on the page. Add the listener only while there are unsaved changes. Remove it as soon as the work is saved.

        Warn only while changes are unsavedPop 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);  }}

        Lines 2 and 3 ask for the warning. Line 8 adds the listener when there are unsaved changes. Line 10 removes it when there are none.

        navigator.sendBeacon(url, data) sends a small piece of data to the server, even while the page is going away. It returns true if the browser accepted the data for sending, or false if not. Use it for small messages such as analytics, which are counts of what users do. Do not wait for a reply, and do not send a large draft with it.

        Keep beacon data smallPop out in the code editor (opens in a new tab)JavaScript
        function queueAnalytics(bytes) {
          return bytes <= 64 * 1024 ? "queued" : "too large";
        }
        
        console.log(queueAnalytics(120));
        console.log(queueAnalytics(70000));

        This model prints queued for a small amount of data and too large for a big one. The 64 KB limit here is only for the model. The real limit is up to the browser, so check the true or false result. Do not assume the data was sent.

        Notice a return from the bfcachePop out in the code editor (opens in a new tab)JavaScript
        function describeReturn(persisted) {
          return persisted ? "restore cached page" : "start a fresh page";
        }
        
        console.log(describeReturn(true));
        console.log(describeReturn(false));

        pageshow fires when a page is shown, including after Back. Its event.persisted is true when the page came from the bfcache. This model copies that: true prints restore cached page, and false, a normal load, prints start a fresh page.

        onload and onerror for resources

        INTERACTIVE

        The whole page has load events. So does each single file. Images, scripts, stylesheets, and iframes each fire their own events:

        • load when the file arrives and works.
        • error when it fails.

        You can set a handler with a property, such as img.onload. addEventListener is usually better: you can add more than one listener and remove them later.

        Show a fallback when an image failsPop out in the code editor (opens in a new tab)JavaScript
        function imageMessage(eventName) {
          return eventName === "load" ? "show image" : "show fallback";
        }
        
        console.log(imageMessage("error"));
        console.log(imageMessage("load"));

        The first call prints show fallback because the image failed. A fallback is something to show instead, such as a plain placeholder box. The second call prints show image.

        In real code, add both listeners before you set src. Otherwise a fast image could finish before your listeners exist.

        One image file either loads or fails. On load, the page shows the image. On error, the page shows a fallback.imageone image fileloadshow imageerrorshow fallback
        An image fires load when it works and error when it fails. Your code picks what to show for each.
        The two resource eventsPop out in the code editor (opens in a new tab)JavaScript
        const events = ["load", "error"];
        for (const event of events) {
          console.log(event + " means a resource outcome");
        }

        This loop prints one line for each event. A file that works fires load. A file that fails, or cannot be read, fires error.

        Load an image with a Promise
        Wrap image loading in a PromisePop 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;  });}
        Resulta real Image object

        Choose an image to load.

        Try it yourself

        Choose an image to load.

        Other files with load and error events, such as scripts and stylesheets, work the same way.

        You can wrap these events in a Promise: an object for a result that comes later, which you can await. Turning event code into a Promise is called promisification. If that idea is new, the Promisification lesson explains it.

        Wrap one image in a PromisePop 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;  });}
        • Line 3 makes a new image.
        • Line 4 resolves the Promise with the image when load fires.
        • Line 5 rejects the Promise with an error when error fires.
        • Line 6 sets src last, so loading starts only after both listeners exist.

        An error event says the file failed, but not why. Show a useful fallback. Do not guess the cause, and do not show the full address of a file that comes from another site.

        Where you will use this

        SORT IT

        These choices show up in everyday code:

        • Start a menu as soon as its elements exist.
        • Wait for a chart image before you measure it.
        • Pause a dashboard while its tab is hidden.
        • Save a small piece of data before the browser freezes the page.
        Match the job to the momentPop out in the code editor (opens in a new tab)JavaScript
        function momentFor(job) {
          if (job === "wire buttons") return "DOMContentLoaded";
          if (job === "pause dashboard") return "visibilitychange";
          return "resource load";
        }
        
        console.log(momentFor("wire buttons"));
        console.log(momentFor("pause dashboard"));
        console.log(momentFor("wait for chart image"));

        Wiring buttons prints DOMContentLoaded. Pausing a dashboard prints visibilitychange. Any other job, such as waiting for a chart image, reaches line 4 and prints resource load.

        The sorter below asks for the moment that fits each task best.

        Which moment?
        • Add click listeners to buttons that are already in the HTML
        • Start a menu script that reads the page's navigation links
        • Read the final size of a large top image after it loads
        • Wait until an embedded iframe has loaded its page
        • Save a draft when the user switches tabs
        • Send a small analytics message 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 moment that fits it best.

        Choose a category for every card. You can change an answer at any time; Reset clears them all.
        A small real startup scriptPop 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());});
        • Line 2 checks whether the HTML is still being read.
        • Line 9 passes in the setup code, which adds a click listener to the Save button.
        • Line 13 listens for the page being hidden or shown. When it is hidden, line 14 sends the draft with sendBeacon.

        Common misconceptions

        • “DOMContentLoaded means everything has loaded.” It means every element exists, and defer and module scripts have finished. Images and iframes may still be loading.
        • “load is always better because it is safer.” It is only later. Use load when you need images or other files, not just elements.
        • “async and defer work on inline scripts.” They only change external classic scripts, the ones with src. An inline classic script runs right where it appears.
        • “async scripts run after DOMContentLoaded.” They may run before it or after it. Each one runs when its download finishes.
        • “beforeunload lets me write my own warning.” Modern browsers show their own general text, and only in some cases.
        • “unload is the right place for cleanup.” It often does not fire, and it can stop the bfcache from working. Use visibilitychange, pagehide, and sendBeacon for small final work.

        Each row of this table is one event: what to use it for, and what not to use it for.

        Lifecycle events that are easy to mix up
        The eventUse it toDo not use it to
        DOMContentLoadedAdd listeners once every element exists.Wait for images or the final layout.
        loadAct once every image and file has loaded.Run startup code that could start earlier.
        visibilitychangePause, resume, or quickly save when the page is hidden.Stop the user from leaving with a warning.
        pagehideReact when the user leaves, and check for the bfcache.Run long cleanup that makes the user wait.
        beforeunloadWarn about unsaved changes, and nothing else.Send analytics, show your own message, or clean up.

        Practice exercises

        5 EXERCISES
        Exercise 1 · Warm-upPredict the ready guard

        The model page's readyState is interactive. 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

          Which event name does the program print?

          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 runs external classic scripts in page order, before DOMContentLoaded?

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

              Exercise 4 · PracticeBroken image handler

              Which image event tells you the image is broken?

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

                Exercise 5 · ChallengeRemove the risky cleanup

                A teammate sends analytics from window.addEventListener("unload", ...). Which event name should they stop using?

                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 program print?

                    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 runs external classic scripts in page order, after the HTML is read 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 event is best for pausing updates 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 sending analytics?

                    Choose an answer to see the explanation.

                  Key takeaways

                  • DOMContentLoaded fires when every element exists. load also waits for images and other files.
                  • Check document.readyState before you add a startup listener, in case the event has already fired.
                  • defer runs external classic scripts in page order, before DOMContentLoaded. async does not keep the order.
                  • Pause and save on visibilitychange or pagehide. Avoid unload.
                  • Use an image's own load and error events to handle that one file.

                  Page lifecycle work means running each piece of JavaScript at the earliest moment when everything it needs is ready.

                  Up next: Form properties & elements.

                  CompleteFrontend Clear concepts. Working examples.