Skip to content

Conversation

@asamuzaK
Copy link
Contributor

@asamuzaK asamuzaK commented Jan 6, 2026

Motivation

This PR refactors CSSStyleDeclaration to better match the internal architecture requirements for jsdom integration.
Key changes include overhauling the constructor, removing the setter for length, and converting internal methods to class methods.

Part of #255 (comment)

Changes

1. Constructor & Initialization Updates

  • Direct Property Assignment: Replaced Object.defineProperties with direct property assignments for initializing internal state, improving readability and performance.
  • Internal Property Cleanup:
    • Renamed _setInProgress to _updating for clarity.
    • Grouped internal properties with comments (e.g., "Internals for jsdom", "Internals for CSS declaration block").

2. Length Property

  • Remove set length: Removed the setter for length. The property is now read-only, consistent with the CSSOM interface.
  • Replace splice: Array.prototype.splice calls were removed because they attempt to set the read-only length property.
    • Implemented _clearIndexedProperties() to handle full clears.
    • Implemented _removeIndexedProperty(index) to handle single item removal and index shifting.
    • Although it is not directly related to length setter, Array.prototype.splice.call(this, property) are also removed and implemented as _getIndexOf(property).

3. Internal Methods Refactoring

  • Class Methods: Moved internal methods (like _setProperty, _borderSetter, _flexBoxSetter, etc.) from Object.defineProperties(CSSStyleDeclaration.prototype, ...) directly into the class definition.
  • Argument Renaming: Renamed the prior argument to priority in all setters for consistency.
    • Also renamed prop to property and val to value.

4. Documentation

  • JSDoc Overhaul: Updated JSDoc comments throughout the file to provide clearer API documentation.

@asamuzaK
Copy link
Contributor Author

asamuzaK commented Jan 6, 2026

const valueObj = parseCSS(

This line runs the parser every time set cssText() is called, so caching it here should result in a slight performance improvement.

However, this contradicts what was previously stated in #271 (comment). What do you think?

Copy link
Member

@domenic domenic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all looks very nice, just one question and a few potential simplifications. I appreciate this small incremental commit.

enumerable: false,
writable: true
},
const { context } = opt;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shorten these lines by doing constructor(onChangeCallback, { context }) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, not making it optional will cause regressions.
I'm planning to change the arguments in a follow-up PR before a major bump, so let us leave it as is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant to type constructor(onChangeCallback, { context } = {})

},
// Internals for jsdom
this._global = globalThis;
this._onChange = typeof onChangeCallback === "function" ? onChangeCallback : null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's simplify

Suggested change
this._onChange = typeof onChangeCallback === "function" ? onChangeCallback : null;
this._onChange = onChangeCallback;

If the caller wants to pass null (or undefined) they can do so directly, instead of allowing any non-function value to trigger this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

throw new this._global.DOMException(msg, name);
}
Array.prototype.splice.call(this, 0, this._length);
this._clearIndexedProperties();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like these improvements to indexed property handling. Centralizing that all makes it much more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bd39426

Please take a look.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, personally I think it was clearer with two functions, but the new version is fine too.

this[prop] = "";
this.removeProperty(prop);
if (Object.hasOwn(propertyDescriptors, property)) {
propertyDescriptors[property].set.call(this, value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is very cool!

Let's add something like

// TODO: refactor handlers to not require `.call()` 

for the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
if (this._values.has(property)) {
const index = this._getIndexOf(property);
// The property already exists but is not indexed into `this` so add it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused how this could happen... Does it ever happen during our tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block has been around for a long time so I've never verified it, but it appears to be dead code.
Removed.

this._priorities.delete(property);
}
this._values.set(property, value);
if (typeof this._onChange === "function" && !this._updating && this.cssText !== originalText) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the above simplification let's just do if (this._onChange && ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, also elsewhere.

@domenic
Copy link
Member

domenic commented Jan 7, 2026

This line runs the parser every time set cssText() is called, so caching it here should result in a slight performance improvement.

However, this contradicts what was previously stated in #271 (comment). What do you think?

First, I think doing any extra caching should be done in a separate PR from this one.

Anyway, my position is similar to that previous comment. We should try to balance the number of caches vs. the performance improvement.

I think the best way to balance this would be to benchmark 3 different setups:

  1. 3 caches: the current set of caches (isValidPropertyValue, parsePropertyValue, and resolveCalc)
  2. 4 caches: the current set of caches plus a parseCSS cache
  3. 2 caches: parseCSS and resolveCalc caches only. (isValidPropertyValue and parsePropertyValue rely on the parseCSS cache, but add a bit of their own work which is not cached.)

When we have those numbers we can make a judgement call. Like, if (3) is 90% as good as (1) or (2), then probably (3) is best. But if (2) gives a 100% performance improvement, then we should just use four caches.

Our benchmarks won't be perfect; in particular, as mentioned in #271 (comment), real-world apps might have worse cache access patterns. But we can react to any reports that people send and add them to the benchmarks and reconsider at that time. Until we get such reports, we can treat the benchmarks as our guide, with a small fudge factor like: 10% worse performance is acceptable if it means fewer caches.

What do you think of this approach?

@asamuzaK asamuzaK force-pushed the style branch 2 times, most recently from 918cb27 to ce4e10f Compare January 7, 2026 16:08
Copy link
Member

@domenic domenic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid extra delay I will integrate the parameter change myself and merge

@domenic domenic merged commit f712f54 into jsdom:main Jan 8, 2026
3 checks passed
@asamuzaK asamuzaK deleted the style branch January 8, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants