Skip to content

Commit c5f2a88

Browse files
kalenikaliaksandrawesomekling
authored andcommitted
LibWeb: Use invalidation sets to reduce style recalculation
Implements idea described in https://docs.google.com/document/d/1vEW86DaeVs4uQzNFI5R-_xS9TcS1Cs_EUsHRSgCHGu8 Invalidation sets are used to reduce the number of elements marked for style recalculation by collecting metadata from style rules about the dependencies between properties that could affect an element’s style. Currently, this optimization is only applied to style invalidation triggered by class list mutations on an element.
1 parent 58c78cb commit c5f2a88

File tree

11 files changed

+549
-3
lines changed

11 files changed

+549
-3
lines changed

Libraries/LibWeb/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ set(SOURCES
8282
CSS/GridTrackPlacement.cpp
8383
CSS/GridTrackSize.cpp
8484
CSS/Interpolation.cpp
85+
CSS/InvalidationSet.cpp
8586
CSS/Length.cpp
8687
CSS/LengthBox.cpp
8788
CSS/MediaList.cpp
@@ -116,6 +117,7 @@ set(SOURCES
116117
CSS/Sizing.cpp
117118
CSS/StyleComputer.cpp
118119
CSS/StyleInvalidation.cpp
120+
CSS/StyleInvalidationData.cpp
119121
CSS/StyleProperty.cpp
120122
CSS/StyleSheet.cpp
121123
CSS/StyleSheetIdentifier.cpp
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include <LibWeb/CSS/InvalidationSet.h>
8+
9+
namespace Web::CSS {
10+
11+
void InvalidationSet::include_all_from(InvalidationSet const& other)
12+
{
13+
m_needs_invalidate_self |= other.m_needs_invalidate_self;
14+
m_needs_invalidate_whole_subtree |= other.m_needs_invalidate_whole_subtree;
15+
for (auto const& property : other.m_properties)
16+
m_properties.set(property);
17+
}
18+
19+
bool InvalidationSet::is_empty() const
20+
{
21+
return !m_needs_invalidate_self && !m_needs_invalidate_whole_subtree && m_properties.is_empty();
22+
}
23+
24+
void InvalidationSet::for_each_property(Function<void(Property const&)> const& callback) const
25+
{
26+
if (m_needs_invalidate_self)
27+
callback({ Property::Type::InvalidateSelf });
28+
if (m_needs_invalidate_whole_subtree)
29+
callback({ Property::Type::InvalidateWholeSubtree });
30+
for (auto const& property : m_properties)
31+
callback(property);
32+
}
33+
34+
}
35+
36+
namespace AK {
37+
38+
unsigned Traits<Web::CSS::InvalidationSet::Property>::hash(Web::CSS::InvalidationSet::Property const& invalidation_set_property)
39+
{
40+
return pair_int_hash(to_underlying(invalidation_set_property.type), invalidation_set_property.name.hash());
41+
}
42+
43+
ErrorOr<void> Formatter<Web::CSS::InvalidationSet::Property>::format(FormatBuilder& builder, Web::CSS::InvalidationSet::Property const& invalidation_set_property)
44+
{
45+
switch (invalidation_set_property.type) {
46+
case Web::CSS::InvalidationSet::Property::Type::InvalidateSelf: {
47+
TRY(builder.put_string("$"sv));
48+
return {};
49+
}
50+
case Web::CSS::InvalidationSet::Property::Type::Class: {
51+
TRY(builder.put_string("."sv));
52+
TRY(builder.put_string(invalidation_set_property.name));
53+
return {};
54+
}
55+
case Web::CSS::InvalidationSet::Property::Type::Id: {
56+
TRY(builder.put_string("#"sv));
57+
TRY(builder.put_string(invalidation_set_property.name));
58+
return {};
59+
}
60+
case Web::CSS::InvalidationSet::Property::Type::TagName: {
61+
TRY(builder.put_string(invalidation_set_property.name));
62+
return {};
63+
}
64+
case Web::CSS::InvalidationSet::Property::Type::Attribute: {
65+
TRY(builder.put_string("["sv));
66+
TRY(builder.put_string(invalidation_set_property.name));
67+
TRY(builder.put_string("]"sv));
68+
return {};
69+
}
70+
case Web::CSS::InvalidationSet::Property::Type::InvalidateWholeSubtree: {
71+
TRY(builder.put_string("*"sv));
72+
return {};
73+
}
74+
default:
75+
VERIFY_NOT_REACHED();
76+
}
77+
}
78+
79+
ErrorOr<void> Formatter<Web::CSS::InvalidationSet>::format(FormatBuilder& builder, Web::CSS::InvalidationSet const& invalidation_set)
80+
{
81+
bool first = true;
82+
invalidation_set.for_each_property([&](auto const& property) {
83+
if (!first)
84+
builder.builder().append(", "sv);
85+
builder.builder().appendff("{}", property);
86+
});
87+
return {};
88+
}
89+
90+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#pragma once
8+
9+
#include <AK/FlyString.h>
10+
#include <AK/Format.h>
11+
#include <AK/Forward.h>
12+
#include <AK/HashTable.h>
13+
#include <AK/Traits.h>
14+
15+
namespace Web::CSS {
16+
17+
class InvalidationSet {
18+
public:
19+
struct Property {
20+
enum class Type : u8 {
21+
InvalidateSelf,
22+
InvalidateWholeSubtree,
23+
Class,
24+
Id,
25+
TagName,
26+
Attribute,
27+
};
28+
29+
Type type;
30+
FlyString name {};
31+
32+
bool operator==(Property const& other) const = default;
33+
};
34+
35+
void include_all_from(InvalidationSet const& other);
36+
37+
bool needs_invalidate_self() const { return m_needs_invalidate_self; }
38+
void set_needs_invalidate_self() { m_needs_invalidate_self = true; }
39+
40+
bool needs_invalidate_whole_subtree() const { return m_needs_invalidate_whole_subtree; }
41+
void set_needs_invalidate_whole_subtree() { m_needs_invalidate_whole_subtree = true; }
42+
43+
void set_needs_invalidate_class(FlyString const& name) { m_properties.set({ Property::Type::Class, name }); }
44+
void set_needs_invalidate_id(FlyString const& name) { m_properties.set({ Property::Type::Id, name }); }
45+
void set_needs_invalidate_tag_name(FlyString const& name) { m_properties.set({ Property::Type::TagName, name }); }
46+
void set_needs_invalidate_attribute(FlyString const& name) { m_properties.set({ Property::Type::Attribute, name }); }
47+
48+
bool is_empty() const;
49+
void for_each_property(Function<void(Property const&)> const& callback) const;
50+
51+
private:
52+
bool m_needs_invalidate_self { false };
53+
bool m_needs_invalidate_whole_subtree { false };
54+
HashTable<Property> m_properties;
55+
};
56+
57+
}
58+
59+
namespace AK {
60+
61+
template<>
62+
struct Traits<Web::CSS::InvalidationSet::Property> : DefaultTraits<String> {
63+
static unsigned hash(Web::CSS::InvalidationSet::Property const&);
64+
static bool equals(Web::CSS::InvalidationSet::Property const& a, Web::CSS::InvalidationSet::Property const& b) { return a == b; }
65+
};
66+
67+
template<>
68+
struct Formatter<Web::CSS::InvalidationSet::Property> : Formatter<StringView> {
69+
ErrorOr<void> format(FormatBuilder&, Web::CSS::InvalidationSet::Property const&);
70+
};
71+
72+
template<>
73+
struct Formatter<Web::CSS::InvalidationSet> : Formatter<StringView> {
74+
ErrorOr<void> format(FormatBuilder&, Web::CSS::InvalidationSet const&);
75+
};
76+
77+
}

Libraries/LibWeb/CSS/StyleComputer.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include <LibWeb/CSS/CSSStyleRule.h>
3838
#include <LibWeb/CSS/CSSTransition.h>
3939
#include <LibWeb/CSS/Interpolation.h>
40+
#include <LibWeb/CSS/InvalidationSet.h>
4041
#include <LibWeb/CSS/Parser/Parser.h>
4142
#include <LibWeb/CSS/SelectorEngine.h>
4243
#include <LibWeb/CSS/StyleComputer.h>
@@ -427,6 +428,46 @@ Vector<MatchingRule> const& StyleComputer::get_hover_rules() const
427428
return m_hover_rules;
428429
}
429430

431+
InvalidationSet StyleComputer::invalidation_set_for_properties(Vector<InvalidationSet::Property> const& properties) const
432+
{
433+
if (!m_style_invalidation_data)
434+
return {};
435+
auto const& descendant_invalidation_sets = m_style_invalidation_data->descendant_invalidation_sets;
436+
InvalidationSet result;
437+
for (auto const& property : properties) {
438+
if (auto it = descendant_invalidation_sets.find(property); it != descendant_invalidation_sets.end())
439+
result.include_all_from(it->value);
440+
}
441+
return result;
442+
}
443+
444+
bool StyleComputer::invalidation_property_used_in_has_selector(InvalidationSet::Property const& property) const
445+
{
446+
if (!m_style_invalidation_data)
447+
return true;
448+
switch (property.type) {
449+
case InvalidationSet::Property::Type::Id:
450+
if (m_style_invalidation_data->ids_used_in_has_selectors.contains(property.name))
451+
return true;
452+
break;
453+
case InvalidationSet::Property::Type::Class:
454+
if (m_style_invalidation_data->class_names_used_in_has_selectors.contains(property.name))
455+
return true;
456+
break;
457+
case InvalidationSet::Property::Type::Attribute:
458+
if (m_style_invalidation_data->attribute_names_used_in_has_selectors.contains(property.name))
459+
return true;
460+
break;
461+
case InvalidationSet::Property::Type::TagName:
462+
if (m_style_invalidation_data->tag_names_used_in_has_selectors.contains(property.name))
463+
return true;
464+
break;
465+
default:
466+
break;
467+
}
468+
return false;
469+
}
470+
430471
Vector<MatchingRule> StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name) const
431472
{
432473
auto const& root_node = element.root();
@@ -2576,6 +2617,11 @@ NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_casca
25762617
return static_cast<CSSNestedDeclarations const&>(rule).parent_style_rule().absolutized_selectors();
25772618
VERIFY_NOT_REACHED();
25782619
}();
2620+
2621+
for (auto const& selector : absolutized_selectors) {
2622+
m_style_invalidation_data->build_invalidation_sets_for_selector(selector);
2623+
}
2624+
25792625
for (CSS::Selector const& selector : absolutized_selectors) {
25802626
MatchingRule matching_rule {
25812627
shadow_root,
@@ -2842,6 +2888,7 @@ void StyleComputer::build_qualified_layer_names_cache()
28422888
void StyleComputer::build_rule_cache()
28432889
{
28442890
m_selector_insights = make<SelectorInsights>();
2891+
m_style_invalidation_data = make<StyleInvalidationData>();
28452892

28462893
if (auto user_style_source = document().page().user_style(); user_style_source.has_value()) {
28472894
m_user_style_sheet = GC::make_root(parse_css_stylesheet(CSS::Parser::ParsingContext(document()), user_style_source.value()));
@@ -2869,6 +2916,7 @@ void StyleComputer::invalidate_rule_cache()
28692916
m_user_agent_rule_cache = nullptr;
28702917

28712918
m_hover_rules.clear_with_capacity();
2919+
m_style_invalidation_data = nullptr;
28722920
}
28732921

28742922
void StyleComputer::did_load_font(FlyString const&)

Libraries/LibWeb/CSS/StyleComputer.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <LibWeb/CSS/CascadedProperties.h>
2020
#include <LibWeb/CSS/ComputedProperties.h>
2121
#include <LibWeb/CSS/Selector.h>
22+
#include <LibWeb/CSS/StyleInvalidationData.h>
2223
#include <LibWeb/Forward.h>
2324
#include <LibWeb/Loader/ResourceLoader.h>
2425

@@ -149,6 +150,9 @@ class StyleComputer {
149150
Vector<MatchingRule> const& get_hover_rules() const;
150151
Vector<MatchingRule> collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional<CSS::Selector::PseudoElement::Type>, bool& did_match_any_hover_rules, FlyString const& qualified_layer_name = {}) const;
151152

153+
InvalidationSet invalidation_set_for_properties(Vector<InvalidationSet::Property> const&) const;
154+
bool invalidation_property_used_in_has_selector(InvalidationSet::Property const&) const;
155+
152156
void invalidate_rule_cache();
153157

154158
Gfx::Font const& initial_font() const;
@@ -278,6 +282,7 @@ class StyleComputer {
278282

279283
OwnPtr<SelectorInsights> m_selector_insights;
280284
Vector<MatchingRule> m_hover_rules;
285+
OwnPtr<StyleInvalidationData> m_style_invalidation_data;
281286
OwnPtr<RuleCache> m_author_rule_cache;
282287
OwnPtr<RuleCache> m_user_rule_cache;
283288
OwnPtr<RuleCache> m_user_agent_rule_cache;

0 commit comments

Comments
 (0)