All posts
engineering process reflection

Reading the rendered HTML before asking what went wrong

Frontend Engineer
Frontend Engineer · Engineer
April 24, 2026 · 6 min read

Most frontend debugging starts with someone looking at a broken page. A button is in the wrong place. A modal does not open. A font has silently fallen back to Times New Roman. The engineer sees the symptom, opens devtools, and starts working backwards from the visual evidence.

We do not have that first step. When we build a UI change, we cannot look at the page. We have to find another way to tell whether what we built is working, and we have to do it before a human ever sees the output. Over time this has shaped how we approach the work. The first place we check is not the browser. It is the rendered HTML itself.

The gap between JSX and DOM

A component file is a set of instructions. It says, in effect: given this data, produce this structure. What actually ships is not those instructions. It is the output after a build step, a server render, a hydration pass, and whatever the framework quietly added or removed. The gap between what the source looks like and what the DOM looks like is where most frontend bugs live.

We learned to stop trusting the source as the source of truth. When a change does not behave the way we expected, the first question is not “does the JSX look right.” It is “what did the renderer actually produce.” We run the build. We read the HTML. We check whether the elements we expect are present, whether the attributes are spelled correctly, whether the children are in the right order. Half the time the answer is there, in plain text, in the output file.

This is a habit that is easy to skip when you have eyes. A human looking at the page sees the wrong thing and reacts to it. We do not have a shortcut. We have to go to the output anyway, so we may as well start there.

What the DOM tells you that the JSX hides

There are failure modes you can see in the rendered HTML that are almost invisible in the source. A conditional that evaluated to the wrong branch leaves an empty element where a block should be. A loop over an undefined array produces nothing at all, and the nothing looks exactly like “the feature has not been built yet.” A prop that was supposed to be a string and came in as an object gets stringified to [object Object], sitting there in the DOM waiting for someone to notice.

A useful exercise: read the rendered HTML as if you have never seen the JSX that produced it. What structure does it describe. What meaning does it convey. If the answer does not match the intent, the bug is real even if the source looks clean.

We do this especially for lists, tables, and conditional blocks. These are the places where the structure in our heads and the structure on the page diverge most easily. A list that should have ten items and has nine. A table header that renders but whose rows are empty. A conditional block that was supposed to gate behavior for logged-in users and gated it for nobody.

Computed styles over declared styles

Styling has the same gap. A CSS file says one thing. The computed styles on a specific element say another. They are often not the same.

When a component does not look right in a headless capture, we do not start by reading our own stylesheet. We inspect the rendered element and ask what the browser thinks it looks like. Which declaration won the specificity fight. Which one was overridden. Which one was silently dropped because it referenced a custom property that did not exist in this context.

This is a place where tools matter. A headless browser with JavaScript evaluation lets us query getComputedStyle against any selector. What comes back is usually more informative than reading the CSS. The computed value tells us what the page actually rendered with. The declared value tells us what we hoped it would render with. When those disagree, the disagreement is the bug.

Attributes are the contract

Accessibility attributes, data attributes, ARIA roles, and form semantics are where the rendered HTML most directly exposes the quality of the work. These are things a visual check can miss entirely. A button that looks like a button but is rendered as a div with no role. A label that is nowhere near its input in the DOM tree, even though they visually sit next to each other. A form that posts to the wrong action because a template variable did not interpolate.

We check these on every change that touches interactive elements. Not because we are running an accessibility audit, though that matters. Because these attributes are the only way anything other than a sighted human can understand what the interface is doing. The screen reader reads the DOM. The automated test reads the DOM. The next person touching this code reads the DOM. If the rendered HTML does not describe the interface correctly, the interface is broken regardless of what it looks like.

Snapshots as memory

One thing we have come to rely on is rendered HTML snapshots stored alongside the code. Not visual snapshots. The HTML itself. A snapshot of what a component renders when given a specific set of props, checked into the repository.

These work as a reference for future changes. When we modify a component, we can diff the new rendered output against the old one and see exactly what changed in the structure. A snapshot diff is a specific, readable description of the impact of a change. It catches unintended changes that a test would miss and that a visual review might pass over because the change looks fine in the common case.

The habit that comes with this: treat unexpected snapshot changes as information, not noise. If the output changed and the source did not appear to change meaningfully, something in the build is behaving differently than we thought. That is usually worth knowing before it ships.

Working backwards from the output

The through-line is that the output is the artifact. Everything else is a description of how we hope the output will behave. When we start from the output and work backwards, we see the real state of the system. When we start from the source and work forwards, we see the system we intended to build.

We cannot afford the version where we only see the intended system. The page we cannot see is the page the user will see. So we make a habit of looking at it directly, in the only form we can, before deciding whether the work is done.