Intersection and Mutation observers explained

Just a guy who loves to write code and watch anime.
Understanding Web Observers
The browser offers several observer APIs that let you monitor and react to specific changes in the DOM. Two of the most useful are Intersection Observer and Mutation Observer. They solve different problems and are used in different scenarios.
Intersection Observer Basics
Intersection Observer detects when an element enters or exits the viewport or intersects with another element. Before this API, developers had to use scroll event listeners with getBoundingClientRect(), which caused performance issues.
How Intersection Observer Works
The Intersection Observer API works by creating an observer that watches a target element and fires a callback when that element intersects with a specified element (usually the viewport).
const observer = new IntersectionObserver(callback, options);
observer.observe(targetElement);
The callback receives an array of IntersectionObserverEntry objects, which contain information about the intersection.
Key Options for Intersection Observer
const options = {
root: null, // The element to use as viewport (null = browser viewport)
rootMargin: "0px", // Margin around the root
threshold: 0.5, // Percentage of target visibility to trigger callback (0-1)
};
Using Intersection Observer in React and TypeScript
Here's how to implement an Intersection Observer in a React component with TypeScript:
import React, { useEffect, useRef, useState } from "react";
interface IntersectionProps {
threshold?: number;
rootMargin?: string;
onIntersect?: () => void;
children: React.ReactNode;
}
const IntersectionComponent: React.FC<IntersectionProps> = ({
threshold = 0,
rootMargin = "0px",
onIntersect,
children,
}) => {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const currentRef = ref.current;
if (!currentRef) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true);
if (onIntersect) onIntersect();
} else {
setIsVisible(false);
}
});
},
{ threshold, rootMargin }
);
observer.observe(currentRef);
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, [threshold, rootMargin, onIntersect]);
return (
<div ref={ref}>
{children}
{isVisible && <div>Element is visible!</div>}
</div>
);
};
Creating a Reusable Intersection Observer Hook
A more flexible approach is to create a custom hook:
import { useEffect, useState, RefObject } from "react";
interface UseIntersectionObserverOptions {
threshold?: number;
rootMargin?: string;
root?: Element | null;
}
function useIntersectionObserver<T extends Element>(
elementRef: RefObject<T>,
options: UseIntersectionObserverOptions = {}
): boolean {
const [isIntersecting, setIsIntersecting] = useState(false);
const { threshold = 0, rootMargin = "0px", root = null } = options;
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);
},
{ threshold, rootMargin, root }
);
observer.observe(element);
return () => {
observer.unobserve(element);
};
}, [elementRef, threshold, rootMargin, root]);
return isIntersecting;
}
// Usage example:
// const divRef = useRef<HTMLDivElement>(null);
// const isVisible = useIntersectionObserver(divRef, { threshold: 0.5 });
Common Use Cases for Intersection Observer
Lazy loading images or content
Infinite scrolling
Playing/pausing videos when they enter/exit viewport
Triggering animations when elements become visible
Implementing scroll-spy navigation
Mutation Observer Basics
Unlike Intersection Observer, Mutation Observer monitors changes to the DOM structure itself. It detects when elements are added, removed, or attributes are changed.
How Mutation Observer Works
The Mutation Observer API works by creating an observer that watches a target node and its descendants, firing a callback whenever changes occur.
const observer = new MutationObserver(callback);
observer.observe(targetNode, options);
The callback receives an array of MutationRecord objects containing information about what changed.
Key Options for Mutation Observer
const options = {
childList: true, // Observe direct children
attributes: true, // Observe attributes
characterData: true, // Observe text content
subtree: true, // Observe all descendants
attributeOldValue: true, // Record previous attribute value
characterDataOldValue: true, // Record previous text
};
Using Mutation Observer in React and TypeScript
Here's how to implement a Mutation Observer in React with TypeScript:
import React, { useEffect, useRef } from "react";
interface MutationProps {
onMutation?: (mutations: MutationRecord[]) => void;
children: React.ReactNode;
}
const MutationComponent: React.FC<MutationProps> = ({
onMutation,
children,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const targetNode = containerRef.current;
if (!targetNode) return;
const config: MutationObserverInit = {
attributes: true,
childList: true,
subtree: true,
};
const observer = new MutationObserver((mutations) => {
if (onMutation) onMutation(mutations);
// Example of processing mutations
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
console.log(
"Child nodes changed:",
mutation.addedNodes.length,
"added"
);
} else if (mutation.type === "attributes") {
console.log("Attribute changed:", mutation.attributeName);
}
});
});
observer.observe(targetNode, config);
return () => {
observer.disconnect();
};
}, [onMutation]);
return <div ref={containerRef}>{children}</div>;
};
MutationRecord
A mutation is a change to the DOM. It can be a change to a node, an attribute, or a text node.
Each MutationRecord contains different information depending on the mutation type:
interface MutationRecord {
type: "attributes" | "characterData" | "childList";
target: Node;
addedNodes: NodeList;
removedNodes: NodeList;
previousSibling: Node | null;
nextSibling: Node | null;
attributeName: string | null;
attributeNamespace: string | null;
oldValue: string | null;
}
For each mutation type:
type: 'childList'- When child nodes are added or removed:addedNodes: NodeList of newly added nodesremovedNodes: NodeList of removed nodestarget: The parent node where changes occurredpreviousSibling: The node before the addition/removal pointnextSibling: The node after the addition/removal pointoldValue: Alwaysnullfor this type
type: 'attributes'- When an element's attribute changes:target: The element whose attribute changedattributeName: Name of the changed attributeattributeNamespace: Namespace of the attribute (if applicable)oldValue: Previous attribute value (only ifattributeOldValue: truewas specified in the observer options)
type: 'characterData'- When text content changes:target: The text node that changed (must be cast toCharacterDatato access current.dataproperty)oldValue: Previous text content (only ifcharacterDataOldValue: truewas specified in the observer options)
Example of processing different mutation types:
function processMutations(mutations: MutationRecord[]) {
mutations.forEach((mutation) => {
switch (mutation.type) {
case "childList":
console.log(
`${mutation.addedNodes.length} nodes added and`,
`${mutation.removedNodes.length} nodes removed at`,
mutation.target
);
break;
case "attributes":
console.log(
`Attribute "${mutation.attributeName}" changed on`,
mutation.target,
`Previous value: ${mutation.oldValue}`
);
break;
case "characterData":
console.log(
`Text content changed from "${mutation.oldValue}" to`,
`"${(mutation.target as CharacterData).data}" on`,
mutation.target
);
break;
}
});
}
Creating a Reusable Mutation Observer Hook
Here's a custom hook for more flexible usage:
import { useEffect, useRef, RefObject } from "react";
interface UseMutationObserverOptions extends MutationObserverInit {
onMutation?: (mutations: MutationRecord[]) => void;
}
function useMutationObserver<T extends Node>(
elementRef: RefObject<T>,
options: UseMutationObserverOptions
): void {
const { onMutation, ...observerOptions } = options;
const callbackRef = useRef(onMutation);
// Update callback ref if it changes
useEffect(() => {
callbackRef.current = onMutation;
}, [onMutation]);
useEffect(() => {
const targetNode = elementRef.current;
if (!targetNode) return;
const observer = new MutationObserver((mutations) => {
if (callbackRef.current) {
callbackRef.current(mutations);
}
});
observer.observe(targetNode, observerOptions);
return () => {
observer.disconnect();
};
}, [elementRef, JSON.stringify(observerOptions)]);
}
// Usage example:
// const divRef = useRef<HTMLDivElement>(null);
// useMutationObserver(divRef, {
// subtree: true,
// childList: true,
// onMutation: (mutations) => console.log(mutations)
// });
Common Use Cases for Mutation Observer
Monitoring form field changes
Tracking DOM changes made by third-party libraries
Implementing custom undo/redo functionality
Observing when dynamically added elements appear
Accessibility enhancements (e.g., announcing when important content changes)
Differences Between Intersection and Mutation Observers
Purpose
Intersection Observer: Detects when elements enter/exit viewport or intersect another element
Mutation Observer: Detects changes to DOM structure or attributes
Performance Impact
Intersection Observer: Very low impact, designed specifically to avoid performance problems
Mutation Observer: Higher impact, especially with subtree: true on large DOM trees
Triggering Conditions
Intersection Observer: Triggers on scrolling, resizing, or element position changes
Mutation Observer: Triggers on DOM modifications (adding/removing nodes, changing attributes)
Browser Support
Intersection Observer: IE not supported, polyfill available
Mutation Observer: Supported in all modern browsers including IE11
When to Use Each Observer
Use Intersection Observer when:
Implementing lazy loading
Creating infinite scroll
Tracking visibility for analytics
Implementing scroll-based animations
Measuring ad impressions
Use Mutation Observer when:
Working with content that changes dynamically
Integrating with libraries that modify the DOM
Building custom editor components
Needing to react to attribute changes
Implementing custom event systems based on DOM changes
Common Pitfalls and Best Practices
For Intersection Observer:
Remember to clean up by unobserving when components unmount
Choose appropriate threshold values for your use case
Be careful with margin values, as they can cause premature intersection
Use the
oncepattern by unobserving after first intersection if you only need to detect one entrance
For Mutation Observer:
Limit the scope of what you're observing to improve performance
Avoid triggering DOM changes inside mutation callbacks (can cause infinite loops)
Be specific with options - only observe what you need
Remember that style changes may not be detected unless they modify inline styles (attributes)
For large applications, consider aggregating mutations before processing them
Conclusion
Both Intersection and Mutation Observers solve specific problems in web development. Intersection Observer helps detect visibility changes efficiently, while Mutation Observer tracks DOM modifications. By understanding their differences and appropriate use cases, you can implement more efficient and responsive web applications.






