The rrweb record() Options That Actually Matter
rrweb’s record() accepts a few dozen options, and if you’re wiring it up yourself, the docs present them as a flat list of equals. They are not equals. Six of the rrweb record options meaningfully change what you ship — your privacy exposure, your CPU cost, and whether your replays render at all. The rest are situational. This is a walkthrough of the six, with the values I’d actually put in production and the reasoning, ending with a full hardened config you can copy.
One framing note before the list: the defaults are tuned for fidelity, not for safety or cost. rrweb the library wants to give you a perfect replay. rrweb in your production app should want something different — a good-enough replay that can’t leak a password and doesn’t tax a low-end phone. Every recommendation below follows from that inversion.
maskAllInputs — the default is a trap
maskAllInputs defaults to false. Out of the box, rrweb records every keystroke into every text field: passwords get some special handling, but names, emails, credit card fields with nonstandard autocomplete attributes, the message someone typed into your support form — captured verbatim, stored in your replay backend, visible to anyone with dashboard access.
Set it to true. Always. Then unmask selectively with maskInputOptions or an unmask class on fields that are genuinely needed for debugging and genuinely harmless — a search box, a quantity field. Allowlist visibility, don’t blocklist secrets. Blocklists fail the first time a designer adds a field you didn’t anticipate, and with PII the failure is silent until it’s a breach disclosure.
While you’re in this neighborhood: maskTextClass and maskTextSelector do the same job for rendered text (account numbers, balances). Use them on anything you’d redact in a screenshot.
blockClass and blockSelector — your performance escape hatch
blockClass (default: "rr-block") stops rrweb from serializing an element and its entire subtree. The replay shows a same-sized placeholder instead.
Two distinct uses. The obvious one is privacy: block the embedded document viewer, the video player, the third-party widget you don’t control. The underrated one is performance: rrweb’s steady-state CPU cost is proportional to DOM mutation volume, and one hot component — a virtualized grid, a live chart re-rendering on a websocket tick — can dominate the entire recording cost. Block it and the recorder goes quiet. I’ve profiled apps where a single rr-block class on a table was the difference between the recorder showing up in the flame chart and not.
blockSelector is the same mechanism as a CSS selector, useful when you can’t edit the third-party markup to add a class.
sampling — throttle the firehose events
Some event types fire at rates far beyond what any human reviewing a replay needs. The sampling option throttles them at the source, which is far cheaper than recording and discarding.
The ones worth setting: mousemove (a number here is the throttle interval in milliseconds — 50ms is indistinguishable from full fidelity when watching a replay), scroll (similar story, ~100ms), and input, where "last" records the final value of a field instead of every intermediate keystroke event. media sampling matters only if you have audio/video elements.
This is pure win. The replay looks identical to human eyes and you’ve cut a large fraction of event volume, which shrinks CPU, memory buffer, and upload size together.
inlineStylesheet — leave it on, know why it exists
inlineStylesheet defaults to true: rrweb inlines the contents of your stylesheets into the snapshot instead of recording a <link> pointing at your CSS URL.
It’s tempting to turn off — snapshots get noticeably smaller. Resist. A replay is watched days or weeks after recording, often after several deploys. If the snapshot references app.83f2c1.css and that hashed file has since been purged from your CDN, the replay renders as an unstyled heap of text. Same failure if your CSS lives behind auth or CORS blocks the replay origin. Broken styling is one of the two classic causes of the “my replay is blank or mangled” ticket — I’ve cataloged the full set of causes in a separate post on fixing blank rrweb replays. Storage is cheap. Debugging a replay you can’t read is not.
slimDOMOptions — free deletions
slimDOMOptions tells the serializer to skip DOM nodes that carry zero visual information: comment nodes, <script> tags (rrweb neutralizes scripts anyway — replays are re-rendered, never re-executed), and various <meta> tags for SEO and social embeds.
Set it to true (the shorthand for “everything safe”). There is no fidelity downside — none of these nodes affect what the replay looks like — and on pages with framework-generated comment markers or long <head> sections, the snapshot shrinks meaningfully. This is the rare config knob with no trade-off attached. The only reason it’s not the default is fidelity-first philosophy.
recordCanvas — off unless you have a specific reason
recordCanvas defaults to false. Keep it there.
DOM diffing is cheap because mutations arrive as structured events. Canvas has no mutation events — it’s pixels — so rrweb has to snapshot canvas contents on an interval, which costs serious CPU and produces heavy image payloads. If your canvas is decorative (a chart the user can also see in the surrounding DOM state, a background animation), recording it buys you nothing. Enable it only when the bug you’re hunting lives inside the canvas — a drawing tool, a WebGL scene — and even then, scope the recording to those pages, not your whole app.
Same conservatism applies to collectFonts and recordCrossOriginIframes: both off unless a specific replay deficiency forces your hand.
A hardened production config
Everything above, assembled:
import { record } from 'rrweb';
const stopFn = record({
emit(event) {
buffer.push(event); // batch + gzip + upload elsewhere
},
// Privacy: allowlist visibility, never blocklist secrets
maskAllInputs: true,
maskTextSelector: '.sensitive, [data-private]',
blockSelector: '.rr-block, iframe[src*="third-party"]',
// Performance
sampling: {
mousemove: 50, // ms throttle
scroll: 100,
input: 'last', // final value only, not every keystroke
},
slimDOMOptions: true,
// Fidelity where it counts
inlineStylesheet: true,
// Expensive extras: off
recordCanvas: false,
collectFonts: false,
});
Add an unmask class for the handful of fields you actively want visible, put rr-block on your heaviest component, and you have a recorder that’s safe by default and cheap by default.
Worth knowing if you’re evaluating build-versus-buy: the commercial and open-source products built on rrweb — LogReplay among them — ship essentially this configuration as their preset, plus the batching, compression, and upload pipeline that emit hand-waves away above. The config is the easy 10% of self-hosting a recorder. But whether you use a product or raw rrweb, you should know what these six options do, because they’re the ones you’ll be reaching for the first time a replay shows something it shouldn’t or costs more than it should.
See the bug the way your user did
LogReplay captures session replays, console output, network requests, and errors in one timeline — so you stop guessing what happened before the ticket arrived.
Try LogReplay free