Native Mobile vs. Web Session Replay: Why Native Is Harder
Web session replay records the DOM. Native mobile has no DOM, so replay tools capture the platform's view hierarchy (or raw screenshots) and reconstruct the screen from that. That single difference is why native replay is genuinely harder: more work on the device, trickier masking, and whole regions of the screen that the recorder simply can't see.
I spend most of my week inside replays, and the mental model I carry into a web session is useless the moment I open a native iOS one. So let me walk through what actually changes, and where native quietly bites you.
Watch what the recorder does at 0:00
Here's a scene I replay in my head whenever someone asks why we can't just "use rrweb on mobile." A user opens an iOS checkout screen. On web, the recorder already has a perfect blueprint: the DOM tree, every node, every style, every input value, serialized once and then patched with tiny deltas as things change. Replaying it later is basically re-running those patches in a headless browser. It's cheap, and it's pixel-faithful because the browser does the rendering for you.
Now the same checkout screen, but native. There's no DOM to serialize. The app drew those buttons with UIKit or Jetpack Compose, straight to a GPU surface. The recorder has to reach into the live view hierarchy, read out every visible view's frame, type, text, and styling, and turn that snapshot into something replayable. PostHog, which built exactly this, described the core problem plainly in its engineering write-up on mobile replay: rrweb for mobile doesn't exist, so the SDK reads the native hierarchy and maps it onto rrweb's wireframe schema when it can. The web gets a recording of changes. Native gets a series of reconstructions.
DOM vs. view hierarchy vs. screenshot
There are really three families of capture, and it helps to name them because vendors blur the lines.
DOM mutation recording (web). The rrweb approach. Full snapshot up front, then a stream of mutation events with microsecond timestamps. Faithful, compact, and it survives responsive layouts because you're replaying the real markup.
View-hierarchy / wireframe capture (native). Read the native view tree, serialize it into boxes, labels, and positions, and render that back as an HTML wireframe on playback. You get structure and text without shipping full images. The catch is that anything the SDK doesn't understand becomes a grey box.
Screenshot capture (native). Grab a bitmap of the screen on a timer and stitch the frames together. Simple, and it handles custom-drawn UI that a wireframe would miss. But the files are heavy, you sample at intervals so you can miss the exact moment something breaks, and every screenshot is a fresh chance to leak whatever text happens to be on screen.
Most serious native SDKs blend the last two. They walk the hierarchy for structure and fall back to rasterizing views they can't interpret. Here's how the trade-offs stack up.
| Dimension | Web (DOM/rrweb) | Native (view hierarchy) | Native (screenshot) |
|---|---|---|---|
| What's captured | DOM tree + mutation deltas | View tree serialized to wireframe | Rendered bitmap on a timer |
| Fidelity | Pixel-faithful, re-rendered | Structural, custom draws may drop | Exactly what was on screen |
| Payload size | Small (text deltas) | Small to medium | Large (images) |
| Custom-drawn UI (Canvas, Compose, games) | Poor (canvas is opaque) | Poor unless rasterized | Good |
| Masking model | CSS selectors / attributes | View type + tags | Region redaction on the image |
| Main-thread cost | Low | Medium to high | High |
| Timing | Continuous (event-driven) | Sampled (per frame) | Sampled (per frame) |
Notice the masking row. On web you can hide a field with a selector and be fairly confident, because the recorder never serializes a masked node's value. Native masking keys off view types and tags, and that gap is where most privacy incidents live. More on that below, because it's the part I lose sleep over.
The four native pitfalls nobody warns you about
Frame rate is a budget you spend on the main thread
Web replay is event-driven, so it costs roughly what the user's activity costs. Native capture is sampled, and each sample can mean walking the whole view tree and rasterizing part of the screen on, or near, the main thread. That's a per-frame tax on the exact thread that's supposed to be keeping your UI at 60fps.
The numbers here are real and recent. Sentry published a detailed post-mortem in August 2025 on its iOS renderer: the original implementation took about 155ms per frame and dropped 9 to 10 of every 60 frames per second while replay was on. Their View Renderer V2 got main-thread work down to roughly 25ms, an ~80% cut, which drops it to 1 or 2 dropped frames only at the capture moment. That's a 5x improvement, and it's the difference between "the app feels fine" and "why does scrolling stutter." Web replay never has this conversation.
The common defense is to sample slowly. Sentry's mobile default, per its performance-overhead docs, is a single frame per second at 1:1 pixel density. One frame a second is plenty to reconstruct a flow and see where someone got stuck. It is not enough to study a fast gesture or a 400ms animation glitch. You're trading fidelity for battery and smoothness, and on native you have to make that trade explicitly.
Battery and thermals, not just CPU
A screenshot every second, encode it, maybe upload it. Do that for a twenty-minute session and the phone notices. Slow sampling, downscaled pixel density, and low-quality encode modes exist precisely because the honest answer to "does this drain battery" is "it can." I've watched sessions where the user's own scrolling got choppy right after a heavy screen loaded, and it wasn't their device being old. It was the recorder. Roughly 1 in every 8 or so sessions I audit on older hardware shows some capture-induced jank if the SDK isn't tuned. Web replay, running in a browser that's already rendering everything, carries none of this weight.
Off-screen and recycled buffers lie to you
Mobile lists recycle views. A RecyclerView or a UICollectionView keeps a handful of real cells and swaps their contents as you scroll. So when the recorder walks the hierarchy, the views that exist are the visible ones, and everything above and below is either not instantiated or holds stale content from a row you already scrolled past. On web, the DOM for a long list is usually all there (or virtualized in a way rrweb still sees as nodes). On native, "what's in the tree" and "what the user saw" drift apart during fast scrolls, and your replay can show a cell that technically never rendered with that data. It's subtle, and it'll make you distrust a session you should trust.
WebViews are a blind spot inside a blind spot
This is the one that surprises people. A native app with an embedded WebView, a checkout page, a help center, an OAuth screen, is running two rendering worlds at once. The native SDK sees the WebView as a single opaque rectangle in the hierarchy. It has no window into the DOM inside it unless a web recorder is also injected in that WebView and the two streams are stitched. Most setups don't do that, so the most sensitive screen in the app, the payment form, is often the one region the replay can't reconstruct. Ironically that can be a privacy win. It's also a coverage hole, and you should know which one you have.
Masking: where native is a step behind
I'll be blunt, because privacy is the part of this job where "mostly works" isn't a passing grade. Native masking is harder than web masking, and the failure modes are quieter.
The good news is that defaults have gotten aggressive. Datadog's mobile privacy options mask all text and input by default and turn on mask_all for images out of the box. That's the right posture. But native masking is tied to view identity, and view identity is fragile across the frameworks people actually ship.
Cross-platform is where it cracks. Sentry's own React Native privacy docs carry an unusually candid warning: view flattening can make Mask and Unmask components not behave as expected and accidentally expose sensitive data, so you must test the app before publishing. Read that again. The framework's own optimization can collapse the view you tagged as "mask this," and your PII sails through. Flutter has a cousin of the same problem, where third-party widgets aren't auto-masked and need manual config. On web, a masked node is masked at serialization time and there's far less between your intent and the outcome.
So the practical rule I give teams: on native, masking is not a checkbox you tick once. It's a thing you verify by opening a real replay of a real form and confirming, with your own eyes, that the card number is a grey block. Do it after every UI refactor. I've seen a "masked" field un-mask itself because someone wrapped it in a new container. It happens more than the marketing pages admit.
Which tool for which surface
If you only ship a web app, rrweb-based tooling is the mature, faithful default and you can mostly ignore this article's second half. If you're native-only, look hardest at masking behavior on your specific framework and at the main-thread cost on your oldest supported device, not the flagship in the demo. If you're both, you want one platform that captures web via rrweb and native via view-hierarchy and lets you watch them side by side, so a "user churned in the mobile app" story and a "user rage-clicked on the site" story live in the same place.
A handful of vendors span both surfaces with honest trade-offs. PostHog and Sentry both do web plus native, with Sentry's engineering notably transparent about the performance work. Datadog and FullStory bring mobile replay into a broader monitoring or DXA suite. Kixo similarly covers web replay via rrweb and native iOS and Android capture with heatmaps and privacy masking, as one of several options for teams that want product analytics and replay together. None of them make the native pitfalls disappear. They just decide how to pay for them, in frames, in battery, in masking effort, and it's worth knowing which bill you're signing up for before you integrate.
The short version
Web replay reconstructs a page from the DOM it recorded, cheaply and faithfully. Native replay reconstructs a screen from a view hierarchy or a screenshot, which costs main-thread time, drains battery, samples instead of streaming, and hands you a masking problem that's easy to get subtly wrong. If you take one habit from this: on native, always verify masking on a real replay of a real form, and always test capture cost on your worst device, not your best. Your users are on the worst device. That's usually where the confused ones are, too.
If you want the vocabulary that keeps these conversations precise, the behavior analytics glossary has the terms I've been leaning on here.