Repository navigation
fix(dom): stop duplicating authored <style> text in cloneNodeAndShadow - #2393
Conversation
clone-dom.js unconditionally set clone.textContent = cssText for every <style> tag, including ones with their own authored text — the generic walkTree call right after then re-cloned the live text node on top of that, doubling the parsed rule count. This defeats serializeCSSOM's identity guard (styleSheetsMatch) whenever the doubled clone sheet doesn't cleanly match the live sheet rule-for-rule (e.g. any unrelated CSSOM mutation elsewhere in the same stylesheet), forcing a full lossy re-serialization via cssRules.map(r => r.cssText) that expands shorthands like `background: var(--x)` into empty longhands. Only synthesize clone.textContent for CSSOM-only sheets that have no text node of their own; a <style> with authored text now gets it once, from walkTree, with no manual assignment. As a side effect this also removes a false-positive data-percy-cssom-serialized marking: the old code set that attribute unconditionally whenever cssText was truthy, even for ordinary <style> tags that were never actually re-serialized from CSSOM. Updated two existing serialize-cssom tests that had encoded this over-marking as expected behavior. Fixes PER-10567 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t the code Drops the explanatory comment blocks added with the fix so clone-dom.js and the tests carry only the behavioural change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude Code PR ReviewPR: #2393 • Head: 9b81343 • Reviewers: code-review (fallback inline checklist — no in-scope installed reviewers discovered) SummaryFixes PER-10567: Review Table
Findings
Verdict: PASS |
…tsMatch The lenA > lenB tolerance and its regression test described clone-dom's <style> text duplication as current behaviour. That duplication is gone, so the comments now state only what the tolerance is for; the guard and the test are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Triage of the review finding
Accepted: the stale comment. Genuine, and this PR is what invalidates it — leaving it would be debt this change created. Fixed in df9b61e. Two places, not one:
Rejected: reconsidering the
So the guard stays exactly as-is. Verified after the change: 496/496 dom tests passing, including both Independently of the review, the PR description now also carries a "Scope — what this does not fix" section: a sheet with any genuine CSSOM delta still fails the guard and still loses |
Claude Code PR ReviewPR: #2393 • Head: df9b61e • Reviewers: code-review (fallback inline checklist — no in-scope installed reviewers discovered) Continues the previous review — changes since SummaryFollow-up commit Review Table
Findings
No new findings. Verdict: PASS |
…2464) * fix(dom): drop comment nodes inside <style> when cloning (PER-11066) Lit leaves empty marker comments in a <style> that contains a binding. Since #2393 they are cloned and serialized as a literal <!----> inside the CSS, which makes the parser drop the following declaration (display:flex in PER-11066, collapsing a gap layout). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(dom): use window.getComputedStyle in PER-11066 test (no-undef) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fixes PER-10567
Root cause
cloneNodeAndShadow'sSTYLEbranch — added in #1884 to capture CSSOM-only (JS-inserted)<style>sheets — setclone.textContent = cssTextfor every<style>, including ones with their own authored text:cloneis a shallow clone (element.cloneNode()), and the genericwalkTree(node.firstChild, clone)call further down re-clones the live text node onto it. So for any<style>with authored text, the CSS was written into the clone twice — once manually here, once bywalkTree.Three consequences:
<style>with authored text shipped twice in the serialized DOM of every snapshot, since March 2025.data-percy-cssom-serialized="true"was set on every non-empty<style>, not only on sheets actually re-serialized from CSSOM — a false positive for essentially every ordinary<style>tag.serializeCSSOM's identity guardstyleSheetsMatch(liveSheet, styleSheetFromNode(cloneOwnerNode)). Before PER-7129: Fix the cssom serialization bug of not accounting for inherit:all css rule #2228 that guard rejected onlenA !== lenB, so a doubled sheet always fell through to the lossycssRules.map(r => r.cssText)path.That lossy path is what produced the reported symptom. Chrome cannot serialize a
var()-containing shorthand that a later longhand partially overrides, so it expands it into pending-substitution longhands that serialize empty:color: transparentandbackground-clip: textsurvive, the gradient does not — the text renders invisible. One customer page produced 278 empty declarations in a single sheet this way. The loss is not recoverable downstream:rule.style.getPropertyValue('background')also returns""for this shape, so the authored text is the only source of truth.Fix
Only synthesize
clone.textContentwhen the<style>has no text node of its own — i.e. CSSOM-only sheets, which is what #1884 was actually for. A<style>with authored text now gets it exactly once, fromwalkTree, and no longer carries the CSSOM marker.Verification
Repro page (
background: var(--gradient); background-clip: text; color: transparent, mirroring the customer rule), served locally, serialized in real Chrome via CDP with the@percy/dombundle built from each branch:9352a73)<style>content<style>contentdata-percy-cssom-serializedmarkersPercy builds on the same page, fix build based on the master build:
So the change is render-neutral while removing ~26% of the serialized DOM on a page with two inline
<style>tags.Tests:
yarn workspace @percy/dom test --karma.browsers ChromeHeadless— 496/496 passing.yarn workspace @percy/dom build— passes.<style>with avar()shorthand survivesserializeDOM()with its text intact, exactly once, and unmarked.serialize-cssomtests had the false-positive marker counts baked in as expectations (3 → 2, 2 → 1) and are corrected.<style>(including@mediarules inserted viainsertRule) is still captured and still marked — covered by the existingpreserves media queries inside CSSOM/maintains order of CSSOM serializationtests, and re-confirmed manually.Scope — what this does not fix
#2228 later relaxed
styleSheetsMatchto only reject whenlenA > lenB, which tolerates a clean doubling. So on 1.32.0+ the duplication no longer forces the lossy path by itself; consequence 3 above only bites on 1.30.x–1.31.x (relevant to products bundling an olderpercyDom.js).A sheet that has any genuine CSSOM delta —
insertRule,deleteRule, or an in-placerule.stylemutation — still fails the guard and still goes through the lossy re-serialization, losingvar()shorthands for the whole sheet. Verified on both master and this branch: the repro page's second<style>(authored text + oneinsertRule) yields 8 empty declarations on both, i.e. the invisible-gradient symptom persists there.That remaining defect is in
serializeCSSOM, notclone-dom: it replaces the clone's authored text wholesale instead of applying only the delta, and the lost value is unrecoverable from CSSOM. It needs a separate fix and is deliberately out of scope here. This PR removes the duplication that made the guard unreliable in the first place, and is a prerequisite for any per-rule delta approach.