Avoid rebuilding shorthands when setting a declaration - #221
Merged
FlorianRappl merged 1 commit intoAug 3, 2026
Merged
Conversation
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.
There was a problem hiding this comment.
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
CreatePropertyhelper that seededRawValuefrom an existing declaration before settingValue. - 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!importantscenarios.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
CssStyleDeclaration.CreatePropertyseeded every newly created property with the raw value of an existing declaration of the same name before returning it:That seed can never be observed:
CreatePropertyhad exactly one caller —SetProperty(name, value, priority).property.Value = propertyValue, and both branches of theCssProperty.Valuesetter assign_valueunconditionally.The lookup is also far from free. On a miss
GetPropertyfalls through toGetPropertyShorthand, i.e.TryCreateShorthand(force: true), which for a shorthand such asbackgroundorborderreconstructs the entire shorthand from its longhands — allocating anICssValue[], recursing per longhand and callingCreateShorthand— only for the result to be overwritten on the next line. Parsing a sheet paid that cost on every declaration.Removing the seed leaves
GetPropertyand the rest of the public behaviour untouched.Results
Sample sheets from
AngleSharp.Performance.Css, BenchmarkDotNet, net10.0:ParseInlineDeclarationsimproves similarly: 968.4 µs → 724.8 µs (−25.2%).ComputedStyleandRenderTreeare unchanged, as expected — they merge declarations viaSetDeclarations/UpdateDeclarationsrather 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
SetPropertyOverwriteTestscovers 28 re-declaration cases: longhand over shorthand and back,!importantin either order, an invalid value after a valid one, an empty value, custom properties,grid-area/grid-row,font,flex,border-radius,overflow,transition,paddingandborder-radiuspartial 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.
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:
CreatePropertyreally had only the one caller (SetProperty(String, String, String)).CssProperty.Value's setter assigns_valueon every path, including the shorthand-with-var()early return.GetPropertyitself 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.