# How uBlock Origin Handles DOM Mutations for Cosmetic Filtering

> Discover how uBlock Origin efficiently handles DOM mutations for cosmetic filtering using MutationObserver and temporary stylesheets to block elements.

- Repository: [Raymond Hill/uBlock](https://github.com/gorhill/uBlock)
- Tags: internals
- Published: 2026-03-05

---

**uBlock Origin watches the live DOM using a `MutationObserver` that batches structural and attribute changes into animation-frame callbacks, routing them to a cosmetic-filtering engine which extracts class and ID hashes, matches them against pre-compiled selector maps, and injects temporary stylesheets with `display:none!important` rules.**

Cosmetic filtering in uBlock Origin must react to dynamic content injection to hide ad elements that appear after the initial page load. The `gorhill/uBlock` repository implements a high-performance, two-layer architecture where a centralized DOM watcher collects mutations and delegates them to specialized filterers that decide which CSS selectors must be applied or re-applied.

## MutationObserver Architecture and Batch Processing

The core observation logic resides in [`src/js/contentscript.js`](https://github.com/gorhill/uBlock/blob/main/src/js/contentscript.js). The `startMutationObserver` function creates a `MutationObserver` instance named `domLayoutObserver` that watches the entire document subtree for structural changes.

```javascript
// src/js/contentscript.js
const startMutationObserver = function() {
    if ( domLayoutObserver !== undefined ) { return; }
    // Create a MutationObserver that receives every DOM change
    domLayoutObserver = new MutationObserver(observerHandler);
    domLayoutObserver.observe(document, {
        childList: true,        // added / removed elements
        subtree: true           // walk the whole tree
    });
    // Batch the work on the next animation frame to avoid thrashing
    safeObserverHandlerTimer = new vAPI.SafeAnimationFrame(safeObserverHandler);
    vAPI.shutdown.add(cleanup);
};

```

The `observerHandler` stores the `addedNodes` and `removedNodes` arrays in temporary buffers (`addedNodeLists`, `removedNodeLists`). When any buffer is non-empty, a **safe animation-frame timer** (`safeObserverHandlerTimer`) schedules the callback. In `safeObserverHandler`, the buffers collapse into plain arrays and each registered listener receives `listener.onDOMChanged(addedNodes, removedNodes)`.

This batching strategy prevents layout thrashing when thousands of nodes mutate simultaneously during page hydration or infinite scroll events.

## The Classic DOMFilterer Implementation

The classic cosmetic filterer (`vAPI.DOMFilterer`) registers itself as a listener via the `vAPI.domWatcher` API:

```javascript
// src/js/contentscript.js
vAPI.domWatcher = { start, addListener, removeListener };
vAPI.domWatcher.addListener(vAPI.DOMFilterer);

```

The filterer implements `onDOMChanged` to handle newly injected content:

```javascript
// src/js/contentscript.js – classic filterer
onDOMChanged: function(addedNodes) {
    if ( addedNodes.length === 0 ) { return; }
    for ( const node of addedNodes ) {
        // If an <iframe> appears, hook its own DOM watcher
        if ( node.localName === 'iframe' ) {
            addIFrame(node);
        }
        // Descend into the subtree, looking for further <iframe>s
        const iframes = node.getElementsByTagName('iframe');
        if ( iframes.length !== 0 ) {
            addIFrames(iframes);
        }
    }
    // Run the procedural cosmetic filter on the new nodes
    process();
}

```

When an iframe element enters the DOM, the filterer recursively attaches observers to the nested document to ensure cosmetic rules apply within sandboxed content.

## MV3 Generic Selector Processing

The Manifest V3 build maintains the same high-level design but isolates the logic in [`platform/mv3/extension/js/scripting/css-generic.js`](https://github.com/gorhill/uBlock/blob/main/platform/mv3/extension/js/scripting/css-generic.js). This implementation creates its own mutation observer focused on attribute changes:

```javascript
// platform/mv3/extension/js/scripting/css-generic.js
let domMutationObserver = new MutationObserver(uBOL_processChanges);
domMutationObserver.observe(document, {
    attributeFilter: [ 'class', 'id' ], // watch class & id changes
    attributes: true,
    childList: true,
    subtree: true,
});

```

The `uBOL_processChanges` callback accumulates mutated nodes into a `pendingNodes` set and schedules processing via `uBOL_processNodes`. For each node, the engine:

1. **Extracts tokens** using `uBOL_idFromNode` and `uBOL_classesFromNode`
2. **Hashes tokens** with a DJB2-variant (`hashFromStr`) identical to the filter compiler
3. **Looks up hashes** in `genericSelectorMap`, which holds pre-computed selector lists
4. **Applies exceptions** if the token matches `genericExceptionSieve`, otherwise pushes selectors to `styleSheetSelectors`

After processing all pending nodes, the engine injects a temporary stylesheet:

```javascript
vAPI.userStylesheet.add(`${selectors.join(',')} { display:none !important; }`);

```

## Observer Lifecycle and Resource Management

Both implementations implement automatic cleanup to prevent memory leaks in idle tabs. The classic version exposes `stopMutationObserver`:

```javascript
// src/js/contentscript.js
const stopMutationObserver = function() {
    if ( domLayoutObserver === undefined ) { return; }
    cleanup();
    vAPI.shutdown.remove(cleanup);
};

```

The MV3 version uses a 20-second inactivity timeout (`needDomChangeObserver`) to disconnect the observer when the DOM stabilizes:

```javascript
// platform/mv3/extension/js/scripting/css-generic.js
const needDomChangeObserver = () => {
    if ( (Date.now() - lastDomChange) > 20000 ) {
        return stopAll('no more DOM changes');
    }
    domChangeTimer = self.setTimeout(needDomChangeObserver, 20000);
};

```

The `stopAll` function clears timers, disconnects the observer, and purges the selector map to free resources.

## Summary

- **Batching architecture**: `MutationObserver` in [`src/js/contentscript.js`](https://github.com/gorhill/uBlock/blob/main/src/js/contentscript.js) buffers mutations and dispatches them on animation frames to minimize reflow costs.
- **Dual implementations**: The classic `vAPI.DOMFilterer` handles procedural filters and iframes, while the MV3 [`css-generic.js`](https://github.com/gorhill/uBlock/blob/main/css-generic.js) uses hash-based matching for generic selectors.
- **Attribute monitoring**: MV3 specifically watches `class` and `id` attributes to catch dynamic CSS class changes that often indicate ad container injection.
- **Resource safety**: Automatic observer shutdown after 20 seconds of inactivity prevents background tab resource exhaustion.

## Frequently Asked Questions

### What triggers a cosmetic filter recalculation in uBlock Origin?

**DOM mutations—specifically `childList` changes for node insertion/removal and `attribute` changes for `class` and `id`—trigger recalculation.** The observer batches these mutations into a single animation-frame callback, then the filterer extracts hashes from new nodes and checks them against the compiled selector map to determine which hide rules to inject.

### How does uBlock Origin handle dynamically added iframes?

**The `onDOMChanged` method in `vAPI.DOMFilterer` detects `iframe` elements in added nodes and calls `addIFrame` to recursively attach DOM watchers to the nested document.** It also descends into added subtrees using `getElementsByTagName('iframe')` to catch nested frames that might not trigger direct mutation records.

### What is the difference between the classic and MV3 mutation handling approaches?

**The classic implementation ([`src/js/contentscript.js`](https://github.com/gorhill/uBlock/blob/main/src/js/contentscript.js)) uses procedural cosmetic filters and handles complex selectors through `vAPI.DOMFilterer`, while the MV3 build ([`platform/mv3/extension/js/scripting/css-generic.js`](https://github.com/gorhill/uBlock/blob/main/platform/mv3/extension/js/scripting/css-generic.js)) relies on pre-compiled hash maps of generic selectors.** The MV3 version optimizes for restricted extension environments by hashing class/id tokens with a DJB2 algorithm and matching against `genericSelectorMap` rather than evaluating CSS selectors in real-time.

### Why does uBlock Origin stop observing DOM mutations after a period of inactivity?

**To conserve memory and CPU in background tabs, the MV3 implementation starts a 20-second timer (`needDomChangeObserver`) that disconnects the `MutationObserver` if no mutations occur.** This `stopAll` cleanup clears the observer, timers, and selector caches, ensuring the extension does not consume resources on static pages.