cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Observers

Learn the browser observer APIs for reacting to visibility, DOM mutations, element resizing, and performance entries without constant polling.

You will be able to
  • 01
    Watch visibilityUse IntersectionObserver with roots, thresholds, root margins, lazy loading, and sentinels.
  • 02
    Read DOM changes in batchesUse MutationObserver options, records, microtask timing, takeRecords, and disconnect.
  • 03
    Observe size and performanceUse ResizeObserver and PerformanceObserver safely with feature detection.

Watchers, not loops

Many browser features are about noticing change. A card becomes visible. A third-party widget adds a node. A component grows wider. A performance measurement is recorded. You could check those things over and over in a timer, but that wastes work and often checks at the wrong time.

The browser gives you a better pattern: observers. You create an observer with a callback, tell it what to watch, and let the browser call you later with entries or records. Each observer specializes in one signal.

Definition

An observer is a browser object that watches a target for a specific kind of change and asynchronously delivers entries or records to your callback.

The four observers in this lesson
ObserverSignal it watchesCallback receivesCommon use
IntersectionObserverVisibility crossing a threshold inside a rootIntersectionObserverEntry objectsLazy loading, scroll reveal, infinite-list sentinels
MutationObserverDOM children, attributes, or text changedMutationRecord objectsIntegrating widgets, detecting attribute changes, editor tools
ResizeObserverAn element's box size changedResizeObserverEntry objectsContainer-driven components, charts, split panes
PerformanceObserverPerformance timeline entries were recordedPerformanceEntry objectsMeasures, marks, and feature-detected web-vitals style metrics

This lesson builds each one with a real lab. The key habit is simple: name the signal first, then choose the observer that watches that signal.

IntersectionObserver: visibility without scroll math

INTERACTIVE

IntersectionObserver tells you when a target crosses visibility thresholds inside a root rectangle. With root: null, the root is the viewport. With root set to a scrollable element, the root is that element. This is the API behind many lazy images, scroll reveal effects, active table-of-contents markers, and infinite lists.

Real-life analogyIntersectionObserver is a lifeguard

Picture a lifeguard assigned to one swimming zone. The lifeguard does not measure every swimmer every millisecond. They call out when a watched swimmer enters or leaves enough of the zone to matter. That is the observer's job.

In real life: A swimming zone
In JavaScript: The root: viewport or scroll box
In real life: A swimmer being watched
In JavaScript: A target element passed to observe()
In real life: How much of the swimmer is inside
In JavaScript: intersectionRatio
In real life: Call out when half the swimmer enters
In JavaScript: threshold: 0.5
In real life: Widen or shrink the zone rope
In JavaScript: rootMargin such as 80px 0px

Where the analogy stops: A lifeguard reacts immediately with human judgment. IntersectionObserver callbacks are asynchronous, can be batched, and report geometry the browser calculated.

Three settings matter most. threshold is a number from 0 to 1, or an array like [0, 0.5, 1]. rootMargin expands or shrinks the root rectangle using px or %. And the first callback is queued soon after observe(), even before the user scrolls, so your code knows the current state.

Visibility lab: thresholds, margins, lazy cards, sentinel
Visibility observer shapePop out in the code editor (opens in a new tab)JavaScript
const observer = new IntersectionObserver(callback, {  root: scrollBox,  threshold: [0, 0.5, 1],  rootMargin: "40px 0px",});observer.observe(card);observer.observe(sentinel);
Real observer output0 log lines
    Try it yourself

    Scroll the lab. The observer will report the current state once after observe, then on threshold crossings. Loaded placeholders: 0. Rendered cards: 8.

    The scroll box is the IntersectionObserver root. The lesson builds its cards with DOM methods inside an empty ref so React does not manage the observed children.

    In the lab, each card starts as a placeholder. When a card intersects, the code swaps in an in-page colored block. The sentinel at the bottom appends more cards when it becomes visible. That is the infinite-list pattern: observe one tiny marker instead of calculating scroll positions yourself.

    MutationObserver: DOM changes as batched records

    STEP THROUGH

    MutationObserver watches a DOM subtree for structural, attribute, and text changes. It is not a replacement for ordinary events. Use events when you control the user action. Use a mutation observer when code you do not directly control changes the DOM, or when a tool needs to audit changes after they happen.

    Real-life analogyMutationObserver is a change log clerk

    Imagine a building clerk writing every change to a list while workers renovate. The clerk does not run to you after each nail. When the current burst of work finishes, you get the list in one batch.

    In real life: The building
    In JavaScript: The observed target node
    In real life: A renovated room, new sign, or changed label
    In JavaScript: Child, attribute, or text mutations
    In real life: The clerk's list
    In JavaScript: Queued MutationRecord objects
    In real life: One batch handed over after the crew pauses
    In JavaScript: One callback after the current script, as a microtask

    Where the analogy stops: A clerk may choose wording freely. MutationObserver records use exact browser-defined fields such as type, target, addedNodes, attributeName, and oldValue.

    The options object decides which records can appear: childList, attributes, subtree, characterData, and old-value flags such as attributeOldValue. At least one of child, attribute, or character data watching must be enabled.

    Step through a MutationObserver batch model
    Step 0 of 9Ready
    Your turn: follow the blue line

    Predict the order. Then step through the pure model of a MutationObserver batch.

    Running in
    1. script
    Next: line 1
    Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
    log.push("sync: start");const records = [];records.push("childList: added item");records.push("attributes: data-state old=closed");records.push("characterData: title changed");log.push("sync: end");log.push("callback: " + records.length + " records");console.log(log.join(" | "));
    CallStoreChangeResultRun = next line. Ran = already executed.
    Recent returnsNothing yet. Start with the blue line.
    Observer options

    Predict first: does the callback appear before or after sync: end?

    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.
    Mutation log: options and batched records
    Pure batching modelPop out in the code editor (opens in a new tab)JavaScript
    const log = [];log.push("sync: start");const records = [];records.push("childList: added item");records.push("attributes: data-state old=closed");records.push("characterData: title changed");log.push("sync: end");log.push("callback: " + records.length + " records");console.log(log.join(" | "));
    Observed node and callback log0 callbacks
      Try it yourself

      Toggle options, then make one change or three synchronous changes.

      The observed card is created inside an empty ref. The callback reports real MutationRecord types from your browser.

      Timing is the important professional detail. MutationObserver callbacks run as microtasks after the current script has finished, which connects directly to the Microtasks in depth lesson. If you need queued records immediately, call takeRecords(). When you are finished, call disconnect() so the observer stops holding onto targets.

      ResizeObserver: element size, not just window size

      INTERACTIVE

      A component can become narrow because a sidebar opened, text wrapped, a parent grid changed, or a font loaded. None of those necessarily fires window.resize. ResizeObserver watches the element itself and runs after layout, before paint.

      Real-life analogyResizeObserver is a tailor

      A good tailor measures the garment, not the room. ResizeObserver does the same for UI: it watches the box you care about, even if the viewport stayed the same.

      In real life: The garment being fitted
      In JavaScript: The observed element
      In real life: A fresh measurement
      In JavaScript: contentRect or borderBoxSize
      In real life: Checking again after the wearer moves
      In JavaScript: A callback after the browser recalculates layout

      Where the analogy stops: A tailor can tug the garment while measuring. A ResizeObserver callback should not resize the observed element directly, or it can trigger a resize loop warning.

      Resize lab: element width, not window width
      ResizeObserver shapePop out in the code editor (opens in a new tab)JavaScript
      const observer = new ResizeObserver((entries) => {  for (const entry of entries) {    console.log(entry.contentRect.width);    console.log(entry.borderBoxSize?.[0]?.inlineSize);  }});observer.observe(panel, { box: "border-box" });
      Container-driven component320px requested
      Narrow layout

      The class changes from React state, not from inside the observer callback.

      Try it yourself

      Move the slider. ResizeObserver will report the element's size after layout.

      The callback only reads the size and updates text. It does not resize the observed element, avoiding a ResizeObserver loop.

      The default box is content-box. You can request border-box or device-pixel-content-box where supported. The older contentRect property is widely available; newer box sizes may be arrays. Read sizes in the callback, then update state or classes carefully. Do not resize the observed element inside its own callback.

      PerformanceObserver: timing cards from the browser

      LAB

      The Performance Timeline records entries: marks, measures, resource timings, navigation timings, and some browser-specific metrics. PerformanceObserver lets you receive those entries as they are recorded. The safest beginner entry type is measure, because you create it yourself with performance.mark() and performance.measure().

      Real-life analogyPerformanceObserver is a race timekeeper

      A race timekeeper does not change how fast the runner moves. They record timing cards. PerformanceObserver is the same: it observes performance data; it does not make code faster by itself.

      In real life: Start and finish flags
      In JavaScript: performance.mark() calls
      In real life: A timing card
      In JavaScript: A PerformanceEntry
      In real life: Handing cards to the coach
      In JavaScript: The observer callback receives entries
      In real life: Older cards on the clipboard
      In JavaScript: buffered: true where supported

      Where the analogy stops: A timekeeper records one race. The Performance Timeline can contain many entry types, and support differs across browsers.

      Performance lab: marks, measures, supported types
      PerformanceObserver shapePop out in the code editor (opens in a new tab)JavaScript
      const observer = new PerformanceObserver((list) => {  for (const entry of list.getEntries()) {    console.log(entry.name + ": " + entry.duration.toFixed(2) + "ms");  }});observer.observe({ entryTypes: ["measure"], buffered: true });performance.mark("work:start");// do a small taskperformance.mark("work:end");performance.measure("demo work", "work:start", "work:end");
      Performance timeline0 measures

      Supported entry types here: Checking after mount...

        Try it yourself

        Run the task to create marks and a measure entry.

        Support varies by browser. Entry type detection runs after mount to avoid server/browser mismatches.

        Feature detection matters. Entry types such as longtask, layout-shift, and largest-contentful-paint are especially Chromium-oriented. Check PerformanceObserver.supportedEntryTypes after mount and observe only types this browser reports.

        Which observer?

        SORT

        The names are long, but the decision is short: visible, changed, resized, or timed? Sort each realistic task by the signal it depends on.

        Which observer fits the task?
        • Load a card image when it scrolls near a list viewport
        • Append another page when a bottom sentinel becomes visible
        • Notice a widget inserted a new child node
        • React when aria-expanded changes
        • Switch a card component when its own width drops below 360px
        • Re-measure a resizable split pane
        • Collect durations from performance.measure()
        • Feature-detect Largest Contentful Paint entries in Chromium
        Try it yourself
        0 of 8 correct

        Read each task, name the signal, then choose visibility, DOM changes, element size, or timings.

        Choose a category for every card. You can change an answer at any time; Reset clears them all.

        Where you'll use this

        Observers shine when the browser already knows the answer better than your code can guess it. Use IntersectionObserver to lazy-load media shortly before it appears, highlight a section link when a heading crosses the viewport, or append more rows when a sentinel appears. Pair it with cleanup: once a one-time image loads, unobserve(image) or disconnect().

        Use MutationObserver at integration boundaries: a rich-text editor, a CMS embed, analytics that records when a third-party widget appears, or a migration script that repairs attributes after a library changes markup. Observe the smallest target and narrow the options. Observing the whole document with subtree: true can produce a noisy stream.

        Use ResizeObserver for container-driven layout. It complements the Sizes, scrolling & coordinates lesson: geometry reads are still real layout facts, but the observer tells you when to read. Use PerformanceObserver for instrumentation: your own marks and measures first, advanced metrics only after feature detection.

        Cleanup rule

        Every observer you create should have a matching cleanup path. Stop watching when the target is gone, the component unmounts, or the one-time job is complete.

        Common misconceptions

        • “Observers run synchronously.” They do not. Intersection callbacks are asynchronous, mutations are delivered in a microtask, ResizeObserver runs after layout, and PerformanceObserver follows the performance timeline.
        • “Threshold 1 means visible at all.” It means the target is fully visible inside the root. Use 0 or a small threshold for “any part entered.”
        • “MutationObserver is an event listener for clicks.” Use events for user actions. Use mutation records for DOM changes after they happen.
        • “Window resize covers component resizing.” A card can resize while the window stays still.
        • “All performance entry types work everywhere.” Some important types are browser-specific. Feature-detect.
        • “Disconnect is optional cleanup.” In long-lived interfaces, missing cleanup can leave stale callbacks and retained targets.

        Practice exercises

        5 EXERCISES
        Exercise 1 · Warm-upPredict the visible ratio

        Read the program and predict the number printed.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function ratio(target, root) {
          const right = Math.min(target.left + target.width, root.left + root.width);
          const left = Math.max(target.left, root.left);
          const bottom = Math.min(target.top + target.height, root.top + root.height);
          const top = Math.max(target.top, root.top);
          const area = Math.max(0, right - left) * Math.max(0, bottom - top);
          return area / (target.width * target.height);
        }
        console.log(ratio({ left: 0, top: 50, width: 100, height: 100 }, { left: 0, top: 0, width: 100, height: 100 }));

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

          Exercise 2 · PracticePredict the mutation-style order

          MutationObserver callbacks use microtask timing. This small program models the same ordering.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const log = [];
          log.push("sync");
          queueMicrotask(() => log.push("observer"));
          log.push("done");
          queueMicrotask(() => console.log(log.join(", ")));

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

            Exercise 3 · PracticeFind the ResizeObserver bug

            Suppose a callback reads panel.contentRect.width and then immediately sets panel.style.width. Should it resize the observed element from inside its own callback?

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

              Exercise 4 · PracticeFormat a performance entry

              Run the snippet mentally. What is the entry type?

              Starter codePop out in the code editor (opens in a new tab)JavaScript
              const entry = { name: "demo", entryType: "measure", duration: 4.567, startTime: 12.34 };
              console.log(entry.entryType + " " + entry.name + ": " + entry.duration.toFixed(2) + "ms");

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

                Exercise 5 · ChallengeDesign a dashboard watcher

                Pick observers for a dashboard with a resizable chart, an infinite activity feed, and a third-party ad slot that inserts markup later.

                  Check your understanding

                  8 QUESTIONS
                  Observers quiz · 8 questionsScore: first tries count
                  1. Question 1 of 8What does root: null mean for IntersectionObserver?

                    Choose an answer to see the explanation.

                  2. Question 2 of 8Which threshold fires when an element becomes at least half visible?

                    Choose an answer to see the explanation.

                  3. Question 3 of 8What does the mutation batching model print?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    const log = [];
                    log.push("sync: start");
                    const records = [];
                    records.push("childList");
                    records.push("attributes");
                    log.push("sync: end");
                    log.push("callback: " + records.length);
                    console.log(log.join(" | "));

                    Choose an answer to see the explanation.

                  4. Question 4 of 8Which MutationObserver method empties pending records immediately?

                    Choose an answer to see the explanation.

                  5. Question 5 of 8When does ResizeObserver deliver notifications in the rendering pipeline?

                    Choose an answer to see the explanation.

                  6. Question 6 of 8Which ResizeObserver box is the default?

                    Choose an answer to see the explanation.

                  7. Question 7 of 8What does this performance formatter print?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    const entry = { name: "demo", entryType: "measure", duration: 4.567, startTime: 12.34 };
                    console.log(entry.entryType + " " + entry.name + ": " + entry.duration.toFixed(2) + "ms at " + entry.startTime.toFixed(1) + "ms");

                    Choose an answer to see the explanation.

                  8. Question 8 of 8Which PerformanceObserver entry types need feature detection because support varies by browser?

                    Choose an answer to see the explanation.

                  Key takeaways

                  • Use observers when the browser can notify you about a change more efficiently than a polling loop.
                  • IntersectionObserver watches visibility inside a root; thresholds can be arrays and rootMargin can preload early.
                  • MutationObserver batches DOM change records and delivers them as a microtask after the current script.
                  • ResizeObserver watches element boxes after layout and before paint; avoid resize loops.
                  • PerformanceObserver watches timeline entries; feature-detect browser-specific entry types.

                  An observer is a focused browser watcher: configure what counts, observe targets, react to asynchronous entries, and disconnect when done.

                  Up next: Animation with JavaScript.

                  CompleteFrontend Clear concepts. Working examples.