Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/add-attribute-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'rrweb': minor
---

Add `attributeFilter` option to `record()` for native MutationObserver attribute filtering.

When set, only mutations to the listed attribute names are observed. Unlisted attributes never fire the JS callback, eliminating CPU overhead from high-frequency style animations (carousels, sliders, animated badges).

```js
record({
emit(event) { /* ... */ },
attributeFilter: ['class', 'value', 'checked', 'selected', 'disabled'],
// 'style' attribute mutations are never observed — zero CPU cost
});
```

When omitted, all attributes are observed (existing default behavior).
2 changes: 2 additions & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function record<T = eventWithTime>(
plugins,
keepIframeSrcFn = () => false,
ignoreCSSAttributes = new Set([]),
attributeFilter,
errorHandler,
} = options;

Expand Down Expand Up @@ -534,6 +535,7 @@ function record<T = eventWithTime>(
processedNodeManager,
canvasManager,
ignoreCSSAttributes,
attributeFilter,
plugins:
plugins
?.filter((p) => p.observer)
Expand Down
11 changes: 9 additions & 2 deletions packages/rrweb/src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,21 @@ export function initMutationObserver(
) => MutationObserver)(
callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer)),
);
observer.observe(rootEl, {
const mutationObserverInit: MutationObserverInit = {
attributes: true,
attributeOldValue: true,
characterData: true,
characterDataOldValue: true,
childList: true,
subtree: true,
});
};
// When attributeFilter is provided, delegate filtering to the browser's
// native MutationObserver. Unlisted attributes never fire the JS callback,
// eliminating CPU overhead from high-frequency style animations.
if (options.attributeFilter) {
mutationObserverInit.attributeFilter = options.attributeFilter;
}
observer.observe(rootEl, mutationObserverInit);
return [observer, iframeCleanup];
}

Expand Down
19 changes: 19 additions & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ export type recordOptions<T> = {
maskTextFn?: MaskTextFn;
slimDOMOptions?: SlimDOMOptions | 'all' | true;
ignoreCSSAttributes?: Set<string>;
/**
* Limit which DOM attributes the MutationObserver watches.
* When set, only mutations to the listed attributes trigger recording.
* Unlisted attributes are never observed — they produce zero CPU overhead.
*
* When omitted or undefined, all attributes are observed (default behavior).
*
* Common use: exclude high-frequency `style` attribute mutations from CSS
* animations while still recording `class`, `value`, and other state changes:
*
* ```
* attributeFilter: ['class', 'value', 'checked', 'selected', 'disabled']
* ```
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#attributefilter
*/
attributeFilter?: string[];
/**
* @deprecated Since 2.0.0. This option is still supported, but is planned to
* be superseded by future captureAssets asset recording APIs.
Expand Down Expand Up @@ -125,6 +142,7 @@ export type observerParam = {
canvasManager: CanvasManager;
processedNodeManager: ProcessedNodeManager;
ignoreCSSAttributes: Set<string>;
attributeFilter?: string[];
plugins: Array<{
observer: (
cb: (...arg: Array<unknown>) => void,
Expand Down Expand Up @@ -159,6 +177,7 @@ export type MutationBufferParam = Pick<
| 'shadowDomManager'
| 'canvasManager'
| 'processedNodeManager'
| 'attributeFilter'
>;

export type ReplayPlugin = {
Expand Down