getComputedStyle inheritance walk is O(2^ancestor-depth) for a custom property set to inherit/unset
getInheritedPropertyValue (lib/jsdom/living/css/helpers/computed-style.js) walks up the ancestors and, for each parent, reads the parent's computed value of the property. A computed read already recurses through every ancestor above that parent. When the value it gets back is falsy the loop moves on to the next parent and does it all again, so the work at depth n is the sum of the work at every shallower depth: T(n) = T(n-1) + T(n-2) + … → 2^n.
A custom property is exactly that case: its initial is "", so a chain of inherit/unset (or nothing set at all) resolves to "" at every level. Custom properties are also excluded from #cachedPropertyValues (propertyDefinitions.has("--x") is false), so nothing memoises the walk. Standard properties never hit it because their initial is truthy.
Measured on jsdom 30.0.1 with #4300 applied (Node 24, reading getPropertyValue("--x") on a warm style cache): depth 16 → 28 ms, 18 → 81 ms, 20 → 307 ms, 21 → 560 ms, 22 → 1 114 ms. Doubling per level; at depth 25 that is ~9 s per property. Before #4300 the cache-hit clone read every property eagerly, which is how antd's --ant-button-only-icon-size{,-sm,-lg}: inherit turned one getComputedStyle(button).display inside an ag-grid into 27 s and broke Testing Library's default *ByRole for us. #4300 hides that entry point; the walk itself is unchanged and any direct read of such a property still pays it.
Reproduction (the read column; hit is flat once #4300 is in):
const { JSDOM } = require("jsdom");
for (const depth of [16, 18, 20, 21, 22]) {
const dom = new JSDOM(`<!doctype html><html><head><style>.leaf { --x: inherit; }</style></head><body></body></html>`);
const { document, getComputedStyle } = dom.window;
let cur = document.body;
for (let i = 0; i < depth - 2; i++) { const d = document.createElement("div"); cur.appendChild(d); cur = d; }
cur.className = "leaf";
void getComputedStyle(cur).display; // fill the style cache
let t = Date.now(); void getComputedStyle(cur).display; const hit = Date.now() - t;
t = Date.now(); getComputedStyle(cur).getPropertyValue("--x"); const read = Date.now() - t;
console.log({ depth, hit, read });
}Suggested fix: when the parent's declaration is a computed one and the property is not colour-typed, its value is already fully inherited, so an empty result means "initial" for the whole chain — return instead of walking further. Verified locally only against the sweep above (0 ms at every depth) and our portal suites; jsdom's own test suite has NOT been run against it — do that before proposing it as a PR. The colour path, which reads specified values with _computed = false, keeps the walk:
} else if (!parent.parentElement || !inherit) {
break;
} else if (!isColor && declaration._computed) {
// The parent's computed value already incorporates every ancestor above it.
break;
}Memoising custom properties in #cachedPropertyValues would be a further, independent improvement.
Source: jsdom/jsdom