Skip to content

Conversation

@domenic
Copy link
Member

@domenic domenic commented Jan 10, 2026

When cssText is set, prepareProperties() already expands border shorthands into all their longhand properties. Previously, calling setProperty() on these shorthands would then trigger their property setters, which called _borderSetter() to expand them again - redundant work.

This makes border property setters check the _updating flag (set during cssText operations) and skip _borderSetter() when true. The setters still call parse() to validate and canonicalize the value, then store it directly via _setProperty().

Affected: border, border-width, border-style, border-color, border-top, border-right, border-bottom, border-left

🤖 Generated with Claude Code


  Parent commit (without optimization):
  - style/createElement + style.cssText: 221 ops/sec
  - style/innerHTML: divs with inline styles: 198 ops/sec

  With optimization:
  - style/createElement + style.cssText: 360 ops/sec (+63%)
  - style/innerHTML: divs with inline styles: 305 ops/sec (+54%)

@asamuzaK I am unsure on this one. It adds a good amount of repetitive code, and is subtle and hard to understand. I guess we can probably factor a bit out to reduce the repetition (I might push some commits to try to do that). But I'm still unsure.

The main thing I'm curious about is how this fits with your larger plans in #255. Do you plan to overhaul these handlers anyway, onto a newer/better architecture? Should we just wait for that? Or would this sort of optimization be valuable longer term?

I also tried to see if this generalizes to other properties besides the border ones, but am not having too much luck so far.

When cssText is set, prepareProperties() already expands border
shorthands into all their longhand properties. Previously, calling
setProperty() on these shorthands would then trigger their property
setters, which called _borderSetter() to expand them again - redundant
work.

This makes border property setters check the _updating flag (set during
cssText operations) and skip _borderSetter() when true. The setters
still call parse() to validate and canonicalize the value, then store
it directly via _setProperty().

Affected: border, border-width, border-style, border-color,
border-top, border-right, border-bottom, border-left

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@domenic domenic force-pushed the perf/skip-border-reexpansion branch from 9d5d906 to a9ed7d0 Compare January 10, 2026 07:14
@asamuzaK
Copy link
Contributor

asamuzaK commented Jan 10, 2026

Looks like there is a regression in test/web-platform-tests/tests/css/cssom/shorthand-values.html

Failed in "The serialization of border: 1px; border-top: 2px; should be canonical.":
assert_equals: expected "border-width: 2px 1px 1px; border-style: none; border-color: currentcolor; border-image: none;" but got "border-width: 1px 1px 1px 2px; border-style: none; border-color: currentcolor; border-image: none;"
Failed in "The serialization of border: 1px; border-top-color: red; should be canonical.":
assert_equals: expected "border-width: 1px; border-style: none; border-color: red currentcolor currentcolor; border-image: none;" but got "border-width: 1px; border-style: none; border-color: currentcolor currentcolor currentcolor red; border-image: none;"

@asamuzaK
Copy link
Contributor

The main thing I'm curious about is how this fits with your larger plans in #255. Do you plan to overhaul these handlers anyway, onto a newer/better architecture? Should we just wait for that? Or would this sort of optimization be valuable longer term?

CSS borders have two shorthands: line (border-color, border-style, border-width) and position (border-top, border-right, border-bottom, border-left).
This makes the process redundant.

At this point, I have no idea how to improve on what is already implemented in normalize.js and parsers.js.

If we can remove this redundancy, that would be very welcome.

@domenic
Copy link
Member Author

domenic commented Jan 11, 2026

CSS borders have two shorthands: line (border-color, border-style, border-width) and position (border-top, border-right, border-bottom, border-left). This makes the process redundant.

At this point, I have no idea how to improve on what is already implemented in normalize.js and parsers.js.

Can you explain a bit more, since you understand this project better than me? In particular, how normalize.js and parsers.js map to the handler files?

For example... Do we expect eventually every CSS property to have its own handler file? Or will we be able to use some generic patterns, like a line handler and a position handler? Or will we maybe generate handlers from the grammars in @webref/css?

@asamuzaK
Copy link
Contributor

There are at least two shorthands that require special handling in CSS.

One is the background, which allows multiple backgrounds.
The other is the border.
While the regular shorthands are simply shorthands for the longhands and have a one-to-one correspondence, e.g. margin is a shorthand for margin-top, margin-right, margin-bottom and margin-left, border has intermediate shorthands in between, and there are two line and position shorthands.

To make things even more confusing, border specifies all four sides with a single value, but border-style and border-top etc. allow you to specify any of sides 1 to 4 individually.

So, If we have the following for example:

border: 1px solid red; border-style: dotted; border-top: 2px double; border-bottom-color: green;

We need to follow these steps in the order the CSS is specified:

  1. First, expand border shorthand to all longhands
  2. Then, override the corresponding longhands with the border-style value
  3. Then, override the corresponding longhands with the border-top value
  4. Then, override border-bottom-color
  5. Finally, if there are any shorthand values that can be specified, set them to those values.

@asamuzaK
Copy link
Contributor

Or will we maybe generate handlers from the grammars in @webref/css?

It would be ideal if that were to happen eventually.

@domenic
Copy link
Member Author

domenic commented Jan 11, 2026

Got it, thanks so much for the explanation!

So my takeaways are:

  • We will probably always need the handler system, but maybe we can reduce its usage over time, until it only applies to very special properties like border and background.
  • Thus, it is probably worth putting extra effort into optimizations for border and background. They will not become obsolete too soon.
  • The PR as-is is not good enough as it causes test failures.
  • It is probably worth doing some work to reduce repetition in the handlers.
    • For example, I think all the color handlers, e.g. border-block-end-color and border-block-start-color, are basically the same. We can probably replace them with something similar to createGenericPropertyDescriptor(), perhaps createColorPropertyDescriptor(). This could be generated by noticing that the grammars are "<color>".
      • Although, border-block-end-color is "<color> | <image-1D>"... I guess the latter works reasonably well now because of the fallback here?
    • We could do the same even for special handlers like border-top and border-left, but it will be more complicated because of all the shorthands.

@asamuzaK
Copy link
Contributor

asamuzaK commented Jan 11, 2026

Although, border-block-end-color is "<color> | <image-1D>"... I guess the latter works reasonably well now because of the fallback here?

Hmm, not quite.
<image-1D> is actually the CSS function stripes().
Therefore, it matches the if (Array.isArray(value) && value.length === 1) { block and is passed to parsers.resolveColorValue(value).
As a result, it is treated as an invalid value and undefined is returned.

Currently, no browsers support stripes(), and there is no test for it in wpt.

While <image-1D> is not a "color", it stores color data internally, similar to gradients, so I'm considering supporting it in cssColor.

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.

3 participants