Skip to content

Commit 8718f17

Browse files
committed
fix(css): prune redundant restyles and refresh pseudo updates
1 parent acdeb15 commit 8718f17

4 files changed

Lines changed: 333 additions & 81 deletions

File tree

webf/lib/src/css/style_declaration.dart

Lines changed: 90 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class CSSStyleDeclaration extends DynamicBindingObject
122122
_pseudoBeforeStyle = newStyle;
123123
target?.markBeforePseudoElementNeedsUpdate();
124124
}
125+
125126
CSSStyleDeclaration? get resolvedPseudoBeforeStyle =>
126127
_resolvePseudoStyle(_pseudoBeforeStyle, _inlinePseudoBeforeStyle);
127128

@@ -132,6 +133,7 @@ class CSSStyleDeclaration extends DynamicBindingObject
132133
_pseudoAfterStyle = newStyle;
133134
target?.markAfterPseudoElementNeedsUpdate();
134135
}
136+
135137
CSSStyleDeclaration? get resolvedPseudoAfterStyle =>
136138
_resolvePseudoStyle(_pseudoAfterStyle, _inlinePseudoAfterStyle);
137139

@@ -144,6 +146,7 @@ class CSSStyleDeclaration extends DynamicBindingObject
144146
// Trigger a layout rebuild so IFC can re-shape text for first-letter styling
145147
target?.markFirstLetterPseudoNeedsUpdate();
146148
}
149+
147150
CSSStyleDeclaration? get resolvedPseudoFirstLetterStyle =>
148151
_resolvePseudoStyle(
149152
_pseudoFirstLetterStyle, _inlinePseudoFirstLetterStyle);
@@ -156,6 +159,7 @@ class CSSStyleDeclaration extends DynamicBindingObject
156159
_pseudoFirstLineStyle = newStyle;
157160
target?.markFirstLinePseudoNeedsUpdate();
158161
}
162+
159163
CSSStyleDeclaration? get resolvedPseudoFirstLineStyle =>
160164
_resolvePseudoStyle(_pseudoFirstLineStyle, _inlinePseudoFirstLineStyle);
161165

@@ -567,14 +571,16 @@ class CSSStyleDeclaration extends DynamicBindingObject
567571

568572
// Validate value.
569573
switch (propertyName) {
570-
case GAP: {
571-
final List<String> tokens = splitByAsciiWhitespacePreservingGroups(normalizedValue);
572-
if (tokens.isEmpty || tokens.length > 2) return false;
573-
for (final token in tokens) {
574-
if (!CSSGap.isValidGapValue(token)) return false;
574+
case GAP:
575+
{
576+
final List<String> tokens =
577+
splitByAsciiWhitespacePreservingGroups(normalizedValue);
578+
if (tokens.isEmpty || tokens.length > 2) return false;
579+
for (final token in tokens) {
580+
if (!CSSGap.isValidGapValue(token)) return false;
581+
}
582+
break;
575583
}
576-
break;
577-
}
578584
case ROW_GAP:
579585
case COLUMN_GAP:
580586
if (!CSSGap.isValidGapValue(normalizedValue)) return false;
@@ -1017,34 +1023,26 @@ class CSSStyleDeclaration extends DynamicBindingObject
10171023

10181024
if (beforeRules.isNotEmpty) {
10191025
pseudoBeforeStyle = cascadeMatchedStyleRules(beforeRules);
1020-
parentElement.markBeforePseudoElementNeedsUpdate();
10211026
} else if (beforeRules.isEmpty && pseudoBeforeStyle != null) {
10221027
pseudoBeforeStyle = null;
1023-
parentElement.markBeforePseudoElementNeedsUpdate();
10241028
}
10251029

10261030
if (afterRules.isNotEmpty) {
10271031
pseudoAfterStyle = cascadeMatchedStyleRules(afterRules);
1028-
parentElement.markAfterPseudoElementNeedsUpdate();
10291032
} else if (afterRules.isEmpty && pseudoAfterStyle != null) {
10301033
pseudoAfterStyle = null;
1031-
parentElement.markAfterPseudoElementNeedsUpdate();
10321034
}
10331035

10341036
if (firstLetterRules.isNotEmpty) {
10351037
pseudoFirstLetterStyle = cascadeMatchedStyleRules(firstLetterRules);
1036-
parentElement.markFirstLetterPseudoNeedsUpdate();
10371038
} else if (firstLetterRules.isEmpty && pseudoFirstLetterStyle != null) {
10381039
pseudoFirstLetterStyle = null;
1039-
parentElement.markFirstLetterPseudoNeedsUpdate();
10401040
}
10411041

10421042
if (firstLineRules.isNotEmpty) {
10431043
pseudoFirstLineStyle = cascadeMatchedStyleRules(firstLineRules);
1044-
parentElement.markFirstLinePseudoNeedsUpdate();
10451044
} else if (firstLineRules.isEmpty && pseudoFirstLineStyle != null) {
10461045
pseudoFirstLineStyle = null;
1047-
parentElement.markFirstLinePseudoNeedsUpdate();
10481046
}
10491047
}
10501048

@@ -1054,6 +1052,27 @@ class CSSStyleDeclaration extends DynamicBindingObject
10541052
..addAll(_properties)
10551053
..addAll(_pendingProperties);
10561054
bool updateStatus = false;
1055+
1056+
void mergePseudoStyle({
1057+
required CSSStyleDeclaration? currentStyle,
1058+
required CSSStyleDeclaration? incomingStyle,
1059+
required void Function(CSSStyleDeclaration? value) assign,
1060+
required VoidCallback markNeedsUpdate,
1061+
required bool clearWhenMissing,
1062+
}) {
1063+
if (incomingStyle != null) {
1064+
if (currentStyle == null) {
1065+
final CSSStyleDeclaration mergedStyle = CSSStyleDeclaration();
1066+
mergedStyle.merge(incomingStyle);
1067+
assign(mergedStyle);
1068+
} else if (currentStyle.merge(incomingStyle)) {
1069+
markNeedsUpdate();
1070+
}
1071+
} else if (clearWhenMissing && currentStyle != null) {
1072+
assign(null);
1073+
}
1074+
}
1075+
10571076
for (String propertyName in properties.keys) {
10581077
CSSPropertyValue? prevValue = properties[propertyName];
10591078
CSSPropertyValue? currentValue = other._pendingProperties[propertyName];
@@ -1091,50 +1110,63 @@ class CSSStyleDeclaration extends DynamicBindingObject
10911110
// 'other' are not dropped when this side is null. When pseudo rules were
10921111
// processed on the other side, clear stale pseudo styles if no rule matches.
10931112
if (other._didProcessPseudoRules) {
1094-
if (other.pseudoBeforeStyle != null) {
1095-
pseudoBeforeStyle ??= CSSStyleDeclaration();
1096-
pseudoBeforeStyle!.merge(other.pseudoBeforeStyle!);
1097-
} else if (pseudoBeforeStyle != null) {
1098-
pseudoBeforeStyle = null;
1099-
}
1100-
1101-
if (other.pseudoAfterStyle != null) {
1102-
pseudoAfterStyle ??= CSSStyleDeclaration();
1103-
pseudoAfterStyle!.merge(other.pseudoAfterStyle!);
1104-
} else if (pseudoAfterStyle != null) {
1105-
pseudoAfterStyle = null;
1106-
}
1107-
1108-
if (other.pseudoFirstLetterStyle != null) {
1109-
pseudoFirstLetterStyle ??= CSSStyleDeclaration();
1110-
pseudoFirstLetterStyle!.merge(other.pseudoFirstLetterStyle!);
1111-
} else if (pseudoFirstLetterStyle != null) {
1112-
pseudoFirstLetterStyle = null;
1113-
}
1114-
1115-
if (other.pseudoFirstLineStyle != null) {
1116-
pseudoFirstLineStyle ??= CSSStyleDeclaration();
1117-
pseudoFirstLineStyle!.merge(other.pseudoFirstLineStyle!);
1118-
} else if (pseudoFirstLineStyle != null) {
1119-
pseudoFirstLineStyle = null;
1120-
}
1113+
mergePseudoStyle(
1114+
currentStyle: pseudoBeforeStyle,
1115+
incomingStyle: other.pseudoBeforeStyle,
1116+
assign: (value) => pseudoBeforeStyle = value,
1117+
markNeedsUpdate: () => target?.markBeforePseudoElementNeedsUpdate(),
1118+
clearWhenMissing: true,
1119+
);
1120+
mergePseudoStyle(
1121+
currentStyle: pseudoAfterStyle,
1122+
incomingStyle: other.pseudoAfterStyle,
1123+
assign: (value) => pseudoAfterStyle = value,
1124+
markNeedsUpdate: () => target?.markAfterPseudoElementNeedsUpdate(),
1125+
clearWhenMissing: true,
1126+
);
1127+
mergePseudoStyle(
1128+
currentStyle: pseudoFirstLetterStyle,
1129+
incomingStyle: other.pseudoFirstLetterStyle,
1130+
assign: (value) => pseudoFirstLetterStyle = value,
1131+
markNeedsUpdate: () => target?.markFirstLetterPseudoNeedsUpdate(),
1132+
clearWhenMissing: true,
1133+
);
1134+
mergePseudoStyle(
1135+
currentStyle: pseudoFirstLineStyle,
1136+
incomingStyle: other.pseudoFirstLineStyle,
1137+
assign: (value) => pseudoFirstLineStyle = value,
1138+
markNeedsUpdate: () => target?.markFirstLinePseudoNeedsUpdate(),
1139+
clearWhenMissing: true,
1140+
);
11211141
} else {
1122-
if (other.pseudoBeforeStyle != null) {
1123-
pseudoBeforeStyle ??= CSSStyleDeclaration();
1124-
pseudoBeforeStyle!.merge(other.pseudoBeforeStyle!);
1125-
}
1126-
if (other.pseudoAfterStyle != null) {
1127-
pseudoAfterStyle ??= CSSStyleDeclaration();
1128-
pseudoAfterStyle!.merge(other.pseudoAfterStyle!);
1129-
}
1130-
if (other.pseudoFirstLetterStyle != null) {
1131-
pseudoFirstLetterStyle ??= CSSStyleDeclaration();
1132-
pseudoFirstLetterStyle!.merge(other.pseudoFirstLetterStyle!);
1133-
}
1134-
if (other.pseudoFirstLineStyle != null) {
1135-
pseudoFirstLineStyle ??= CSSStyleDeclaration();
1136-
pseudoFirstLineStyle!.merge(other.pseudoFirstLineStyle!);
1137-
}
1142+
mergePseudoStyle(
1143+
currentStyle: pseudoBeforeStyle,
1144+
incomingStyle: other.pseudoBeforeStyle,
1145+
assign: (value) => pseudoBeforeStyle = value,
1146+
markNeedsUpdate: () => target?.markBeforePseudoElementNeedsUpdate(),
1147+
clearWhenMissing: false,
1148+
);
1149+
mergePseudoStyle(
1150+
currentStyle: pseudoAfterStyle,
1151+
incomingStyle: other.pseudoAfterStyle,
1152+
assign: (value) => pseudoAfterStyle = value,
1153+
markNeedsUpdate: () => target?.markAfterPseudoElementNeedsUpdate(),
1154+
clearWhenMissing: false,
1155+
);
1156+
mergePseudoStyle(
1157+
currentStyle: pseudoFirstLetterStyle,
1158+
incomingStyle: other.pseudoFirstLetterStyle,
1159+
assign: (value) => pseudoFirstLetterStyle = value,
1160+
markNeedsUpdate: () => target?.markFirstLetterPseudoNeedsUpdate(),
1161+
clearWhenMissing: false,
1162+
);
1163+
mergePseudoStyle(
1164+
currentStyle: pseudoFirstLineStyle,
1165+
incomingStyle: other.pseudoFirstLineStyle,
1166+
assign: (value) => pseudoFirstLineStyle = value,
1167+
markNeedsUpdate: () => target?.markFirstLinePseudoNeedsUpdate(),
1168+
clearWhenMissing: false,
1169+
);
11381170
}
11391171

11401172
return updateStatus;

webf/lib/src/dom/document.dart

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,52 @@ class _PendingInteractivePseudoUpdate {
6262
}
6363
}
6464

65+
@visibleForTesting
66+
List<MapEntry<Element, bool>> pruneNestedDirtyStyleElements(
67+
Iterable<MapEntry<Element, bool>> dirtyElements) {
68+
final List<MapEntry<Element, bool>> dirtyList = dirtyElements.toList();
69+
if (dirtyList.length <= 1) {
70+
return dirtyList;
71+
}
72+
73+
final Set<int> rebuildNestedAddresses = <int>{};
74+
for (final dirty in dirtyList) {
75+
if (!dirty.value) {
76+
continue;
77+
}
78+
final Pointer? pointer = dirty.key.pointer;
79+
if (pointer != null) {
80+
rebuildNestedAddresses.add(pointer.address);
81+
}
82+
}
83+
84+
if (rebuildNestedAddresses.isEmpty) {
85+
return dirtyList;
86+
}
87+
88+
final List<MapEntry<Element, bool>> effectiveDirty =
89+
<MapEntry<Element, bool>>[];
90+
for (final dirty in dirtyList) {
91+
bool coveredByAncestorSubtreeRecalc = false;
92+
Element? ancestor = dirty.key.parentElement;
93+
while (ancestor != null) {
94+
final Pointer? ancestorPtr = ancestor.pointer;
95+
if (ancestorPtr != null &&
96+
rebuildNestedAddresses.contains(ancestorPtr.address)) {
97+
coveredByAncestorSubtreeRecalc = true;
98+
break;
99+
}
100+
ancestor = ancestor.parentElement;
101+
}
102+
103+
if (!coveredByAncestorSubtreeRecalc) {
104+
effectiveDirty.add(dirty);
105+
}
106+
}
107+
108+
return effectiveDirty;
109+
}
110+
65111
class Document extends ContainerNode {
66112
final WebFController controller;
67113
late AnimationTimeline animationTimeline;
@@ -588,8 +634,9 @@ class Document extends ContainerNode {
588634
}
589635

590636
dynamic querySelector(List<dynamic> args) {
591-
if (args[0].runtimeType == String && (args[0] as String).isEmpty)
637+
if (args[0].runtimeType == String && (args[0] as String).isEmpty) {
592638
return null;
639+
}
593640
return query_selector.querySelector(this, args.first);
594641
}
595642

@@ -624,8 +671,9 @@ class Document extends ContainerNode {
624671
}
625672

626673
dynamic getElementById(List<dynamic> args) {
627-
if (args[0].runtimeType == String && (args[0] as String).isEmpty)
674+
if (args[0].runtimeType == String && (args[0] as String).isEmpty) {
628675
return null;
676+
}
629677
final elements = elementsByID[args.first];
630678
if (elements == null || elements.isEmpty) {
631679
return null;
@@ -907,12 +955,28 @@ class Document extends ContainerNode {
907955
if (recalcFromRoot) {
908956
documentElement?.recalculateStyle(rebuildNested: true);
909957
} else {
958+
final List<MapEntry<Element, bool>> resolvedDirty =
959+
<MapEntry<Element, bool>>[];
910960
for (int address in _styleDirtyElements) {
911961
Element? element = ownerView
912962
.getBindingObject(Pointer.fromAddress(address)) as Element?;
963+
if (element == null) {
964+
continue;
965+
}
913966
final bool rebuildNested =
914967
_styleDirtyElementsRebuildNested.contains(address);
915-
element?.recalculateStyle(rebuildNested: rebuildNested);
968+
resolvedDirty.add(MapEntry(element, rebuildNested));
969+
}
970+
971+
// Child-list batching can mark both an ancestor and many descendants
972+
// dirty in the same microtask. If an ancestor is already going to
973+
// rebuild its subtree, separately recalc-ing descendants repeats the
974+
// same recursive walk and dominates large popup mounts.
975+
final List<MapEntry<Element, bool>> effectiveDirty =
976+
pruneNestedDirtyStyleElements(resolvedDirty);
977+
978+
for (final dirty in effectiveDirty) {
979+
dirty.key.recalculateStyle(rebuildNested: dirty.value);
916980
}
917981
}
918982
_styleDirtyElements.clear();

0 commit comments

Comments
 (0)