Skip to content

Avoid rebuilding shorthands when setting a declaration - #221

Merged
FlorianRappl merged 1 commit into
AngleSharp:develfrom
jafin:perf/avoid-shorthand-rebuild-on-setproperty
Aug 3, 2026
Merged

Avoid rebuilding shorthands when setting a declaration#221
FlorianRappl merged 1 commit into
AngleSharp:develfrom
jafin:perf/avoid-shorthand-rebuild-on-setproperty

Conversation

@jafin

@jafin jafin commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

What

CssStyleDeclaration.CreateProperty seeded every newly created property with the raw value of an existing declaration of the same name before returning it:

var newProperty = _context.CreateProperty(propertyName);
var existing = GetProperty(propertyName);
if (existing is not null) newProperty.RawValue = existing.RawValue;

That seed can never be observed:

  1. CreateProperty had exactly one caller — SetProperty(name, value, priority).
  2. Its very next statement is property.Value = propertyValue, and both branches of the CssProperty.Value setter assign _value unconditionally.

The lookup is also far from free. On a miss GetProperty falls through to GetPropertyShorthand, i.e. TryCreateShorthand(force: true), which for a shorthand such as background or border reconstructs the entire shorthand from its longhands — allocating an ICssValue[], recursing per longhand and calling CreateShorthand — only for the result to be overwritten on the next line. Parsing a sheet paid that cost on every declaration.

Removing the seed leaves GetProperty and the rest of the public behaviour untouched.

Results

Sample sheets from AngleSharp.Performance.Css, BenchmarkDotNet, net10.0:

Sample Time before Time after Alloc before Alloc after
cdnjs.cloudflare 1.963 ms 1.499 ms (−23.6%) 1379.8 KB 1226.6 KB (−11.1%)
static.licdn 1.904 ms 1.588 ms (−16.6%) 1095.9 KB 972.1 KB (−11.3%)
maxcdn.bootstrapcdn 7.811 ms 6.522 ms (−16.5%) 4978.0 KB 4419.8 KB (−11.2%)
csszengarden 1.743 ms 1.446 ms (−17.0%) 1283.2 KB 1178.1 KB (−8.2%)
florian-rappl 4.137 ms 3.683 ms (−11.0%) 2268.8 KB 2077.5 KB (−8.4%)
s.yimg 1.935 ms 1.807 ms (−6.6%) 1032.2 KB 1000.5 KB (−3.1%)
style.aliunicorn 1.507 ms 1.434 ms (−4.8%) 897.8 KB 876.1 KB (−2.4%)

ParseInlineDeclarations improves similarly: 968.4 µs → 724.8 µs (−25.2%).

ComputedStyle and RenderTree are unchanged, as expected — they merge declarations via SetDeclarations/UpdateDeclarations rather than going through this setter.

The gains track the allocation profile: the two files that improve least (style.aliunicorn, s.yimg) are the tokenizer-dominated sheets, where tokenization is 78% and 72% of parse allocation and declarations are comparatively few.

Correctness

SetPropertyOverwriteTests covers 28 re-declaration cases: longhand over shorthand and back, !important in either order, an invalid value after a valid one, an empty value, custom properties, grid-area/grid-row, font, flex, border-radius, overflow, transition, padding and border-radius partial overrides.

The expectations are a baseline captured from the previous implementation, which produced byte-identical output for all 28 cases. So the test is a genuine differential against the old behaviour, not a restatement of the new one. It is labelled as a captured baseline rather than a claim about what the spec requires.

  • Full suite: 1951 passing.
  • Builds clean on all target frameworks: netstandard2.0, net8.0, net10.0, net462, net472.

Notes for review

The load-bearing claim is that the removed seed is dead. Worth confirming both halves independently:

  • CreateProperty really had only the one caller (SetProperty(String, String, String)).
  • CssProperty.Value's setter assigns _value on every path, including the shorthand-with-var() early return.

GetProperty itself is deliberately not touched — the forced-shorthand behaviour on a lookup miss is still there for the public API, and only the internal seeding call is gone.

CssStyleDeclaration.CreateProperty seeded every newly created property
with the raw value of an existing declaration of the same name before
handing it back:

    var newProperty = _context.CreateProperty(propertyName);
    var existing = GetProperty(propertyName);
    if (existing is not null) newProperty.RawValue = existing.RawValue;

That seed can never be observed. CreateProperty had a single caller,
SetProperty(name, value, priority), whose next statement is
`property.Value = propertyValue` - and both branches of the
CssProperty.Value setter assign _value unconditionally.

The lookup itself is not cheap. On a miss GetProperty falls through to
GetPropertyShorthand, i.e. TryCreateShorthand(force: true), which for a
shorthand such as background or border reconstructs the whole shorthand
from its longhands - allocating an ICssValue[], recursing per longhand
and calling CreateShorthand - only for the result to be overwritten on
the next line. Parsing a sheet paid for that on every declaration.

Dropping the seed leaves GetProperty and the rest of the public
behaviour untouched.

Measured on the sample sheets in AngleSharp.Performance.Css (net10.0):

  cdnjs.cloudflare      1.963 ms -> 1.499 ms   1379.8 KB -> 1226.6 KB
  csszengarden          1.743 ms -> 1.446 ms   1283.2 KB -> 1178.1 KB
  florian-rappl         4.137 ms -> 3.683 ms   2268.8 KB -> 2077.5 KB
  maxcdn.bootstrapcdn   7.811 ms -> 6.522 ms   4978.0 KB -> 4419.8 KB
  s.yimg                1.935 ms -> 1.807 ms   1032.2 KB -> 1000.5 KB
  static.licdn          1.904 ms -> 1.588 ms   1095.9 KB ->  972.1 KB
  style.aliunicorn      1.507 ms -> 1.434 ms    897.8 KB ->  876.1 KB

Inline declaration parsing improves by a similar margin (968.4 us ->
724.8 us); the cascade benchmarks are unchanged, as they merge
declarations instead of going through this setter.

SetPropertyOverwriteTests covers 28 re-declaration cases - longhand over
shorthand and back, !important in either order, an invalid value after a
valid one, an empty value, custom properties, grid-area/grid-row, font,
flex and border-radius. The expectations are a baseline captured from the
previous implementation, which produced identical output for all of them.
Copilot AI review requested due to automatic review settings August 2, 2026 23:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR removes an unobservable “seeding” step in CssStyleDeclaration.SetProperty(...) that previously fetched an existing declaration (potentially forcing expensive shorthand reconstruction) before immediately overwriting the new property’s value. It fits into AngleSharp.Css’s CSSOM declaration-setting path, reducing allocations and parse-time work when parsing large stylesheets.

Changes:

  • Removed the private CreateProperty helper that seeded RawValue from an existing declaration before setting Value.
  • Updated SetProperty(...) to create the property directly via _context.CreateProperty(propertyName) without any prior lookup.
  • Added a differential regression test suite (SetPropertyOverwriteTests) to pin re-declaration behavior across a range of shorthand/longhand and !important scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs Removes the pre-lookup/seeding path when setting a declaration to avoid unnecessary shorthand rebuilding work.
src/AngleSharp.Css.Tests/Declarations/SetPropertyOverwriteTests.cs Adds targeted tests asserting re-declaration results match the previous behavior baseline across multiple overwrite patterns.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@FlorianRappl FlorianRappl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@FlorianRappl
FlorianRappl merged commit 3f82d35 into AngleSharp:devel Aug 3, 2026
5 checks passed
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