Skip to content

Commit 5d0d98e

Browse files
committed
perf(css): optimize pure Dart style matching and cascade
1 parent 8718f17 commit 5d0d98e

12 files changed

Lines changed: 974 additions & 471 deletions

webf/lib/src/css/cascade.dart

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,32 @@ int compareStyleRulesForCascade(CSSStyleRule a, CSSStyleRule b,
110110
CSSStyleDeclaration cascadeMatchedStyleRules(List<CSSStyleRule> rules) {
111111
final declaration = CSSStyleDeclaration();
112112
if (rules.isEmpty) return declaration;
113+
if (rules.length == 1) {
114+
declaration.union(rules.first.declaration);
115+
return declaration;
116+
}
113117

114118
final normalOrder = List<CSSStyleRule>.from(rules)
115119
..sort((a, b) => compareStyleRulesForCascade(a, b, important: false));
120+
121+
final bool hasImportantDeclarations =
122+
rules.any((rule) => rule.declaration.hasImportantDeclarations);
123+
if (!hasImportantDeclarations) {
124+
for (final r in normalOrder) {
125+
declaration.union(r.declaration);
126+
}
127+
return declaration;
128+
}
129+
116130
for (final r in normalOrder) {
117131
declaration.unionByImportance(r.declaration, important: false);
118132
}
119133

120-
final importantOrder = List<CSSStyleRule>.from(rules)
121-
..sort((a, b) => compareStyleRulesForCascade(a, b, important: true));
134+
final bool hasLayeredRules = rules.any((rule) => rule.layerOrderKey != null);
135+
final List<CSSStyleRule> importantOrder = hasLayeredRules
136+
? (List<CSSStyleRule>.from(rules)
137+
..sort((a, b) => compareStyleRulesForCascade(a, b, important: true)))
138+
: normalOrder;
122139
for (final r in importantOrder) {
123140
declaration.unionByImportance(r.declaration, important: true);
124141
}

webf/lib/src/css/css_rule.dart

Lines changed: 203 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,212 @@
77
* Copyright (C) 2022-2024 The WebF authors. All rights reserved.
88
*/
99

10-
import 'package:quiver/core.dart';
11-
import 'package:collection/collection.dart';
1210
import 'package:webf/css.dart';
1311
import 'package:webf/src/foundation/logger.dart';
1412

13+
bool cssRuleListsStructurallyEqual(List<CSSRule> left, List<CSSRule> right) {
14+
if (identical(left, right)) return true;
15+
if (left.length != right.length) return false;
16+
for (int index = 0; index < left.length; index++) {
17+
if (!cssRulesStructurallyEqual(left[index], right[index])) {
18+
return false;
19+
}
20+
}
21+
return true;
22+
}
23+
24+
int cssRuleListStructuralHash(Iterable<CSSRule> rules) {
25+
return Object.hashAll(rules.map(cssRuleStructuralHash));
26+
}
27+
28+
bool cssRulesStructurallyEqual(CSSRule? left, CSSRule? right) {
29+
if (identical(left, right)) return true;
30+
if (left == null || right == null) return left == right;
31+
if (left.runtimeType != right.runtimeType) return false;
32+
33+
if (left is CSSStyleRule && right is CSSStyleRule) {
34+
return left.selectorGroup.structurallyEquals(right.selectorGroup) &&
35+
left.layerPath.length == right.layerPath.length &&
36+
_stringListsEqual(left.layerPath, right.layerPath) &&
37+
left.declaration.structurallyEquals(right.declaration);
38+
}
39+
40+
if (left is CSSLayerStatementRule && right is CSSLayerStatementRule) {
41+
return _layerNamePathsEqual(left.layerNamePaths, right.layerNamePaths);
42+
}
43+
44+
if (left is CSSLayerBlockRule && right is CSSLayerBlockRule) {
45+
return left.name == right.name &&
46+
_stringListsEqual(left.layerNamePath, right.layerNamePath) &&
47+
cssRuleListsStructurallyEqual(left.cssRules, right.cssRules);
48+
}
49+
50+
if (left is CSSImportRule && right is CSSImportRule) {
51+
return left.href == right.href && left.media == right.media;
52+
}
53+
54+
if (left is CSSKeyframesRule && right is CSSKeyframesRule) {
55+
return left.name == right.name &&
56+
_keyframesEqual(left.keyframes, right.keyframes);
57+
}
58+
59+
if (left is CSSFontFaceRule && right is CSSFontFaceRule) {
60+
return left.declarations.structurallyEquals(right.declarations);
61+
}
62+
63+
if (left is CSSMediaDirective && right is CSSMediaDirective) {
64+
return _mediaQueriesEqual(left.cssMediaQuery, right.cssMediaQuery) &&
65+
cssRuleListsStructurallyEqual(
66+
left.rules ?? const <CSSRule>[], right.rules ?? const <CSSRule>[]);
67+
}
68+
69+
return left.cssText == right.cssText;
70+
}
71+
72+
int cssRuleStructuralHash(CSSRule rule) {
73+
if (rule is CSSStyleRule) {
74+
return Object.hash(
75+
rule.type,
76+
rule.selectorGroup.structuralHashCode,
77+
Object.hashAll(rule.layerPath),
78+
rule.declaration.structuralHashCode,
79+
);
80+
}
81+
82+
if (rule is CSSLayerStatementRule) {
83+
return Object.hash(
84+
rule.type,
85+
Object.hashAll(rule.layerNamePaths.map((path) => Object.hashAll(path))),
86+
);
87+
}
88+
89+
if (rule is CSSLayerBlockRule) {
90+
return Object.hash(
91+
rule.type,
92+
rule.name,
93+
Object.hashAll(rule.layerNamePath),
94+
cssRuleListStructuralHash(rule.cssRules),
95+
);
96+
}
97+
98+
if (rule is CSSImportRule) {
99+
return Object.hash(rule.type, rule.href, rule.media);
100+
}
101+
102+
if (rule is CSSKeyframesRule) {
103+
return Object.hash(
104+
rule.type,
105+
rule.name,
106+
Object.hashAll(rule.keyframes.map((keyframe) => Object.hash(
107+
keyframe.property,
108+
keyframe.value,
109+
keyframe.offset,
110+
keyframe.easing,
111+
))),
112+
);
113+
}
114+
115+
if (rule is CSSFontFaceRule) {
116+
return Object.hash(rule.type, rule.declarations.structuralHashCode);
117+
}
118+
119+
if (rule is CSSMediaDirective) {
120+
return Object.hash(
121+
rule.type,
122+
_mediaQueryStructuralHash(rule.cssMediaQuery),
123+
cssRuleListStructuralHash(rule.rules ?? const <CSSRule>[]),
124+
);
125+
}
126+
127+
return Object.hash(rule.type, rule.cssText);
128+
}
129+
130+
bool _layerNamePathsEqual(List<List<String>> left, List<List<String>> right) {
131+
if (identical(left, right)) return true;
132+
if (left.length != right.length) return false;
133+
for (int index = 0; index < left.length; index++) {
134+
if (!_stringListsEqual(left[index], right[index])) return false;
135+
}
136+
return true;
137+
}
138+
139+
bool _stringListsEqual(List<String> left, List<String> right) {
140+
if (identical(left, right)) return true;
141+
if (left.length != right.length) return false;
142+
for (int index = 0; index < left.length; index++) {
143+
if (left[index] != right[index]) return false;
144+
}
145+
return true;
146+
}
147+
148+
bool _keyframesEqual(List<Keyframe> left, List<Keyframe> right) {
149+
if (identical(left, right)) return true;
150+
if (left.length != right.length) return false;
151+
for (int index = 0; index < left.length; index++) {
152+
final Keyframe leftKeyframe = left[index];
153+
final Keyframe rightKeyframe = right[index];
154+
if (leftKeyframe.property != rightKeyframe.property ||
155+
leftKeyframe.value != rightKeyframe.value ||
156+
leftKeyframe.offset != rightKeyframe.offset ||
157+
leftKeyframe.easing != rightKeyframe.easing) {
158+
return false;
159+
}
160+
}
161+
return true;
162+
}
163+
164+
bool _mediaQueriesEqual(CSSMediaQuery? left, CSSMediaQuery? right) {
165+
if (identical(left, right)) return true;
166+
if (left == null || right == null) return left == right;
167+
if (left._mediaUnary != right._mediaUnary) return false;
168+
if (left._mediaType?.name != right._mediaType?.name) return false;
169+
if (left.expressions.length != right.expressions.length) return false;
170+
for (int index = 0; index < left.expressions.length; index++) {
171+
final CSSMediaExpression leftExpression = left.expressions[index];
172+
final CSSMediaExpression rightExpression = right.expressions[index];
173+
if (leftExpression.op != rightExpression.op) return false;
174+
final Map<String, String>? leftStyle = leftExpression.mediaStyle;
175+
final Map<String, String>? rightStyle = rightExpression.mediaStyle;
176+
if (leftStyle == null || rightStyle == null) {
177+
if (leftStyle != rightStyle) return false;
178+
continue;
179+
}
180+
if (leftStyle.length != rightStyle.length) return false;
181+
final List<String> keys = leftStyle.keys.toList(growable: false)..sort();
182+
final List<String> otherKeys = rightStyle.keys.toList(growable: false)
183+
..sort();
184+
if (!_stringListsEqual(keys, otherKeys)) return false;
185+
for (final String key in keys) {
186+
if (leftStyle[key] != rightStyle[key]) return false;
187+
}
188+
}
189+
return true;
190+
}
191+
192+
int _mediaQueryStructuralHash(CSSMediaQuery? mediaQuery) {
193+
if (mediaQuery == null) return 0;
194+
return Object.hash(
195+
mediaQuery._mediaUnary,
196+
mediaQuery._mediaType?.name,
197+
Object.hashAll(mediaQuery.expressions.map((expression) {
198+
final Map<String, String>? mediaStyle = expression.mediaStyle;
199+
if (mediaStyle == null) {
200+
return Object.hash(expression.op, null);
201+
}
202+
final List<String> keys = mediaStyle.keys.toList(growable: false)..sort();
203+
return Object.hash(
204+
expression.op,
205+
Object.hashAll(keys.map((key) => Object.hash(key, mediaStyle[key]))),
206+
);
207+
})),
208+
);
209+
}
210+
15211
/// https://drafts.csswg.org/cssom/#the-cssstylerule-interface
16212
class CSSStyleRule extends CSSRule {
17213
@override
18-
String get cssText => '${selectorGroup.selectorText} {${declaration.cssText}}';
214+
String get cssText =>
215+
'${selectorGroup.selectorText} {${declaration.cssText}}';
19216

20217
@override
21218
int get type => CSSRule.STYLE_RULE;
@@ -25,12 +222,10 @@ class CSSStyleRule extends CSSRule {
25222

26223
CSSStyleRule(this.selectorGroup, this.declaration) : super();
27224

28-
@override
29-
int get hashCode => hash2(selectorGroup, declaration);
225+
int get structuralHashCode => cssRuleStructuralHash(this);
30226

31-
@override
32-
bool operator ==(Object other) {
33-
return hashCode == other.hashCode;
227+
bool structurallyEquals(CSSStyleRule other) {
228+
return cssRulesStructurallyEqual(this, other);
34229
}
35230
}
36231

@@ -58,29 +253,6 @@ class CSSLayerStatementRule extends CSSRule {
58253

59254
@override
60255
int get type => CSSRule.LAYER_STATEMENT_RULE;
61-
62-
static bool _deepEquals(List<List<String>> a, List<List<String>> b) {
63-
if (identical(a, b)) return true;
64-
if (a.length != b.length) return false;
65-
for (var i = 0; i < a.length; i++) {
66-
final ai = a[i];
67-
final bi = b[i];
68-
if (ai.length != bi.length) return false;
69-
for (var j = 0; j < ai.length; j++) {
70-
if (ai[j] != bi[j]) return false;
71-
}
72-
}
73-
return true;
74-
}
75-
76-
@override
77-
int get hashCode => hashObjects(layerNamePaths.map((p) => hashObjects(p)));
78-
79-
@override
80-
bool operator ==(Object other) {
81-
return other is CSSLayerStatementRule &&
82-
_deepEquals(layerNamePaths, other.layerNamePaths);
83-
}
84256
}
85257

86258
/// Represents a CSS Cascade Layer block: `@layer name { ... }`.
@@ -107,30 +279,6 @@ class CSSLayerBlockRule extends CSSRule {
107279
@override
108280
int get type => CSSRule.LAYER_BLOCK_RULE;
109281

110-
@override
111-
int get hashCode => hashObjects(<Object?>[
112-
name,
113-
hashObjects(layerNamePath),
114-
hashObjects(cssRules),
115-
]);
116-
117-
static bool _listEquals(List<String> a, List<String> b) {
118-
if (identical(a, b)) return true;
119-
if (a.length != b.length) return false;
120-
for (int i = 0; i < a.length; i++) {
121-
if (a[i] != b[i]) return false;
122-
}
123-
return true;
124-
}
125-
126-
@override
127-
bool operator ==(Object other) {
128-
return other is CSSLayerBlockRule &&
129-
other.name == name &&
130-
_listEquals(other.layerNamePath, layerNamePath) &&
131-
const ListEquality<CSSRule>().equals(other.cssRules, cssRules);
132-
}
133-
134282
int insertRule(CSSRule rule, int index) {
135283
if (index < 0 || index > cssRules.length) {
136284
throw RangeError.index(index, cssRules, 'index');

0 commit comments

Comments
 (0)