SalakCode SalakCode
Browser APIs

Mutation Observer

React to DOM changes efficiently

Advanced
browser api dom debugging

Definition

MutationObserver provides developers a way to react to changes in the DOM. It monitors the DOM tree for changes and triggers callbacks when mutations occur, providing a performant alternative to deprecated DOM mutation events.

Basic Usage

Creating an Observer

const observer = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    console.log('Mutation type:', mutation.type);
    console.log('Target:', mutation.target);
    
    if (mutation.type === 'childList') {
      console.log('Added:', mutation.addedNodes);
      console.log('Removed:', mutation.removedNodes);
    }
    
    if (mutation.type === 'attributes') {
      console.log('Attribute:', mutation.attributeName);
      console.log('Old value:', mutation.oldValue);
    }
  });
});

// Start observing
observer.observe(targetNode, {
  childList: true,      // Watch for child additions/removals
  attributes: true,     // Watch for attribute changes
  characterData: true,  // Watch for text content changes
  subtree: true,        // Watch descendants too
  attributeOldValue: true,
  characterDataOldValue: true
});

Mutation Types

Child List Changes

const container = document.getElementById('container');

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach(node => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          console.log('Element added:', node.tagName);
        }
      });
      
      mutation.removedNodes.forEach(node => {
        console.log('Element removed:', node);
      });
    }
  });
});

observer.observe(container, { childList: true });

// Changes that will be detected:
container.appendChild(document.createElement('div'));
container.removeChild(container.firstChild);
container.innerHTML = '<p>New content</p>';

Attribute Changes

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'attributes') {
      console.log(`Attribute ${mutation.attributeName} changed`);
      console.log('Old value:', mutation.oldValue);
      console.log('New value:', mutation.target.getAttribute(mutation.attributeName));
    }
  });
});

observer.observe(element, {
  attributes: true,
  attributeOldValue: true,
  attributeFilter: ['class', 'id', 'data-*'] // Only specific attributes
});

Practical Use Cases

Form Validation Feedback

const form = document.querySelector('form');

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.target.classList.contains('error')) {
      showErrorMessage(mutation.target);
    }
  });
});

form.querySelectorAll('input').forEach(input => {
  observer.observe(input, { 
    attributes: true, 
    attributeFilter: ['class'] 
  });
});

Auto-Resize Textarea

function autoResize(textarea) {
  const resize = () => {
    textarea.style.height = 'auto';
    textarea.style.height = textarea.scrollHeight + 'px';
  };
  
  const observer = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
      if (mutation.type === 'characterData') {
        resize();
      }
    });
  });
  
  observer.observe(textarea, {
    characterData: true,
    subtree: true
  });
  
  textarea.addEventListener('input', resize);
}

Debug Tool

function watchElement(element, label = 'Element') {
  const observer = new MutationObserver((mutations) => {
    console.group(`${label} mutated:`);
    mutations.forEach(mutation => {
      console.log('Type:', mutation.type);
      console.log('Target:', mutation.target);
      if (mutation.addedNodes.length) {
        console.log('Added:', mutation.addedNodes);
      }
      if (mutation.removedNodes.length) {
        console.log('Removed:', mutation.removedNodes);
      }
    });
    console.groupEnd();
  });
  
  observer.observe(element, {
    childList: true,
    attributes: true,
    characterData: true,
    subtree: true
  });
  
  return () => observer.disconnect();
}

// Usage
const cleanup = watchElement(document.body, 'Body');
// ...later
cleanup();

Advanced Patterns

Batch Processing

const pendingMutations = [];
let timeoutId = null;

const observer = new MutationObserver((mutations) => {
  pendingMutations.push(...mutations);
  
  clearTimeout(timeoutId);
  timeoutId = setTimeout(() => {
    processBatch(pendingMutations);
    pendingMutations.length = 0;
  }, 100); // Batch over 100ms
});

function processBatch(mutations) {
  // Process all mutations at once
  const addedElements = new Set();
  
  mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType === Node.ELEMENT_NODE) {
        addedElements.add(node);
      }
    });
  });
  
  // Do something with all added elements
  addedElements.forEach(initializeElement);
}

Reacting to Specific Changes

function waitForElement(selector, parent = document.body) {
  return new Promise((resolve) => {
    // Check if already exists
    const element = parent.querySelector(selector);
    if (element) {
      resolve(element);
      return;
    }
    
    const observer = new MutationObserver((mutations) => {
      const element = parent.querySelector(selector);
      if (element) {
        observer.disconnect();
        resolve(element);
      }
    });
    
    observer.observe(parent, { childList: true, subtree: true });
  });
}

// Usage
waitForElement('.dynamic-content').then(el => {
  console.log('Element appeared:', el);
});

Performance Considerations

Scope Observation

// Bad: Observing entire document
observer.observe(document.body, { 
  childList: true, 
  subtree: true 
});

// Good: Observe specific container
observer.observe(specificContainer, { 
  childList: true 
});

// Better: Use attributeFilter
observer.observe(element, {
  attributes: true,
  attributeFilter: ['class', 'style']
});

Disconnect When Done

const observer = new MutationObserver(callback);
observer.observe(target, config);

// Always disconnect when not needed
function cleanup() {
  observer.disconnect();
}

// In frameworks
useEffect(() => {
  const observer = new MutationObserver(callback);
  observer.observe(ref.current, config);
  
  return () => observer.disconnect();
}, []);

Limitations

// Things MutationObserver cannot detect:

// 1. CSS pseudo-class changes (:hover, :focus)
// 2. CSS property changes
// 3. Form input value changes (use 'input' event)
// 4. Scroll position changes
// 5. Window resize

// For CSS changes, use getComputedStyle polling or ResizeObserver
// For form values, use input/change events
// For scroll/resize, use scroll/resize events or IntersectionObserver
Key Takeaway

MutationObserver is powerful for detecting DOM changes but should be used sparingly due to performance implications. Always scope observations tightly, disconnect when done, and batch process mutations. Consider if your use case could be solved with events or other APIs instead.

Resources

Related Topics