← Go back
JavaScript 6/10/2026

Observe · Subscribe · React

Observers in JavaScript and how they can be used

Understanding Observers in JS

A lot of frontend APIs are event-driven, but there is a slightly different category of APIs that quietly powers many modern UI patterns: observers.

Instead of repeatedly checking whether something changed, observers allow the browser to notify your code only when a specific change actually happens. In practice, this means less polling, fewer manual event listeners, better performance, cleaner reactive logic.

The general idea looks almost identical across all observer APIs. The browser starts watching something internally, and your callback runs whenever that thing changes:

const observer = new SomeObserver((entries) => {
  // react to changes
});

observer.observe(target);

MutationObserver, watching the DOM itself

This observer listens for DOM changes: elements being added or removed, attribute changes, text updates.

const observer = new MutationObserver((mutations) => {
  console.log("DOM changed", mutations);
});

observer.observe(document.body, {
  childList: true,
  subtree: true,
});

This becomes useful whenever the DOM changes outside your direct control: third-party widgets inject HTML, a CMS dynamically renders content, browser extensions modify the page, chat messages appear in real time.

Without it, developers often resort to polling. That works, but it is inefficient and unnecessarily repetitive:

setInterval(() => {
  const modal = document.querySelector(".modal");
}, 500);

What’s important: if you already control the application state properly, for example in React, using DOM observers to infer state is usually a bad abstraction.

IntersectionObserver, detecting visibility

This is easily the most commonly used observer. It tracks whether an element enters or leaves the viewport. A huge amount of modern UI behavior is built on top of this API.

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log("Element became visible");
    }
  });
});

observer.observe(document.querySelector(".card"));

Examples:

  • lazy-loading images
  • infinite scrolling
  • scroll animations
  • autoplaying videos only when visible
  • analytics for element visibility

ResizeObserver, watching element dimensions

Reacts whenever an element changes size.

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log(entry.contentRect.width);
  }
});

observer.observe(document.querySelector(".container"));

At first glance this may sound similar to window.onresize, but the difference is important. he browser window can stay exactly the same size while individual elements still resize because:

  • flexbox recalculated layout
  • content expanded
  • a sidebar collapsed
  • fonts loaded
  • grid dimensions changed

This API is especially useful for responsive components, charts and canvases, dynamic layouts, editors and dashboards. It is one of those APIs that quietly removes a lot of awkward layout logic.

PerformanceObserver, does what it says

This one is different. Instead of UI changes, it listens to browser performance metrics.

const observer = new PerformanceObserver((list) => {
  console.log(list.getEntries());
});

observer.observe({ entryTypes: ["paint"] });

This API is heavily used in:

  • performance monitoring
  • analytics
  • Core Web Vitals tracking
  • debugging rendering issues

For example:

  • Largest Contentful Paint (LCP)
  • First Contentful Paint (FCP)
  • layout shifts
  • long tasks

Many production monitoring tools are built around this API.

What’s also crucial to understand?

1. Observers are fundamentally reactive

All these APIs share is the same mental model.

Instead of asking repeatedly:

Did something change?

We register interest once:

Tell me when it changes.

The browser becomes responsible for the observation process itself.

That distinction matters because the browser can usually perform these checks far more efficiently than userland polling logic.

Always clean up!

Observers continue running until disconnected. That means cleanup is essential, especially in React components.

useEffect(() => {
  const observer = new IntersectionObserver(...);

  observer.observe(ref.current);

  return () => observer.disconnect();
}, []);

Without cleanup you can end up with:

  • duplicate callbacks
  • memory leaks
  • stale references

This is especially noticeable in SPAs where components mount / unmount all the time.

Final thoughts

Observers are one of those browser features that quietly changed how modern frontend applications are built.

A lot of behaviors that previously required:

  • scroll listeners
  • resize listeners
  • intervals
  • repeated DOM queries

Can now be expressed declaratively through browser-managed observation. And in many cases the biggest advantage is not just cleaner code, but allowing the browser itself to decide how observation should be optimized internally.