Concepts 2026-05-19

DOM Snapshots: How a Page Becomes Data

A common misconception: a DOM snapshot is a picture of a web page. It isn’t. There are no pixels in it at all. A DOM snapshot is a structured copy of the page’s element tree — every tag, attribute, and text node, serialized to JSON at one instant in time. A screenshot tells you what a page looked like; a DOM snapshot tells you what the page was, in enough detail that a machine can build it again.

That distinction is the foundation under session replay, DOM-diffing test tools, and a good chunk of browser devtools. It gets explained badly all the time, so let’s do it once, properly.

What a DOM snapshot actually contains

The browser holds your page in memory as a tree of nodes. Serializing it means walking that tree — depth-first, starting at the document root — and converting each node into a plain object a JSON encoder can handle.

For an element node, the serializer records the tag name, the attributes as a key–value map, and a list of children. For a text node, just the text. Each node also gets an integer ID, assigned in walk order, which is how later systems (like a replay’s mutation stream) refer back to specific nodes without fragile selectors.

This markup:

<div class="alert alert-error" role="alert">
  Payment failed. <a href="/support">Contact support</a>
</div>

becomes roughly this:

{
  "type": 2, "tagName": "div", "id": 41,
  "attributes": { "class": "alert alert-error", "role": "alert" },
  "childNodes": [
    { "type": 3, "id": 42, "textContent": "Payment failed. " },
    { "type": 2, "tagName": "a", "id": 43,
      "attributes": { "href": "/support" },
      "childNodes": [
        { "type": 3, "id": 44, "textContent": "Contact support" }
      ]
    }
  ]
}

Verbose, yes. But it’s text, it compresses extremely well, and unlike an image it’s queryable — you can ask a snapshot “was the error banner present?” with a tree lookup.

If you want one mental model to keep: a DOM snapshot is a floor plan, not a photograph. A photograph shows you one angle at one resolution. A floor plan records the structure — and from a floor plan, you can rebuild the room.

Captured, referenced, or rebuilt

Not everything on a page can be — or should be — copied into the snapshot. Serializers sort page content into three buckets:

Bucket What Why
Captured Tags, attributes, text nodes, inline styles Small, textual, essential structure
Referenced Images, video, fonts Embedding binaries would bloat snapshots enormously; a URL suffices
Rebuilt Stylesheets The live CSS rules are read out and re-serialized as text

The first bucket is straightforward. The second is a pragmatic trade: an <img> is stored as its src URL, and the replaying browser fetches it like any other page would. The cost of that trade is rot — if the asset is deleted or lives behind authentication, old snapshots replay with broken images. Some tools offer asset caching to pin copies server-side; without it, a snapshot’s visual fidelity has a shelf life even though its structure is permanent.

Stylesheets are the interesting bucket. You might expect the serializer to just record <link rel="stylesheet" href="..."> and move on. It usually doesn’t, for two reasons. First, the same rot problem — CSS files get renamed on every deploy (app.8f3a1c.css), so the URL is stale within days. Second, stylesheets can be modified at runtime: CSS-in-JS libraries inject rules directly via the CSSOM, the browser’s object model for styles, and those rules exist nowhere in any fetchable file. So the serializer reads the parsed rules out of document.styleSheets and writes the actual CSS text into the snapshot. The stylesheet is rebuilt from the browser’s in-memory truth, not linked from its original source. (One consequence: cross-origin stylesheets without CORS headers can’t be read this way and may be silently missing.)

The state the DOM won’t tell you

Here’s where naive serialization falls apart. Some of the most important page state isn’t in the markup at all.

Input values. When a user types into a text field, the DOM attribute value doesn’t change — only the element’s live JavaScript property does. Serialize the markup and every form comes out blank. Snapshotters have to explicitly read element.value (and checked for checkboxes, selectedIndex for selects) and write them into the serialized node as if they were attributes. This is also exactly where privacy masking hooks in, since inputs are where the sensitive stuff lives.

Scroll positions. Scroll offsets live on elements as scrollTop/scrollLeft properties, invisible to markup. They’re read at snapshot time and recorded per-node, otherwise every replay would start pinned to the top of every container.

Ephemeral rendering state. Which element has focus, text selection, CSS :hover — some of this is captured with extra effort, some is simply lost. A snapshot is faithful to the tree, not to every last bit of browser state hanging off it.

Why scripts are stripped — and replay is inert

The serializer deliberately drops <script> contents (typically replacing them with a placeholder tag so the tree shape is preserved) and strips inline handlers like onclick.

This can feel like a loss. It’s the opposite: it’s the property that makes the whole system trustworthy.

The snapshot already contains the outcome of every script that ran — whatever your JavaScript did to the page is sitting right there in the serialized tree. Re-running the code during replay would at best duplicate effects that are already recorded, and at worst fire real network requests, mutate real data, or execute a user’s injected content inside your dashboard. So replay players go further than stripping: they reconstruct the tree inside a sandboxed iframe with script execution disabled, as a second line of defense.

A replayed page is a museum diorama of the original — perfectly arranged, completely inert. You can inspect it, but nothing in it can act.

Why one concept carries so much

Once a page is data, everything downstream is ordinary engineering. You can diff two snapshots to detect visual regressions. You can index them and search for “sessions where the error banner rendered.” You can record one snapshot plus a stream of small mutations and get session replay for a fraction of the bandwidth of video. None of those systems are doing anything exotic — they’re all leaning on the same move. The page became a tree of JSON, and trees of JSON are something we know how to store, compare, and query.

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