mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-28 11:49:44 +00:00
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.
This commit is contained in:
parent
58c78cb003
commit
c5f2a88f69
Notes:
github-actions[bot]
2025-01-19 18:55:55 +00:00
Author: https://github.com/kalenikaliaksandr
Commit: c5f2a88f69
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3292
Reviewed-by: https://github.com/awesomekling
11 changed files with 549 additions and 3 deletions
192
Libraries/LibWeb/CSS/StyleInvalidationData.cpp
Normal file
192
Libraries/LibWeb/CSS/StyleInvalidationData.cpp
Normal file
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/GenericShorthands.h>
|
||||
#include <LibWeb/CSS/Selector.h>
|
||||
#include <LibWeb/CSS/StyleInvalidationData.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
// Iterates over the given selector, grouping consecutive simple selectors that have no combinator (Combinator::None).
|
||||
// For example, given "div:not(.a) + .b[foo]", the callback is invoked twice:
|
||||
// once for "div:not(.a)" and once for ".b[foo]".
|
||||
template<typename Callback>
|
||||
static void for_each_consecutive_simple_selector_group(Selector const& selector, Callback callback)
|
||||
{
|
||||
auto const& compound_selectors = selector.compound_selectors();
|
||||
int compound_selector_index = compound_selectors.size() - 1;
|
||||
Vector<Selector::SimpleSelector const&> simple_selectors;
|
||||
Selector::Combinator combinator = Selector::Combinator::None;
|
||||
bool is_rightmost = true;
|
||||
while (compound_selector_index >= 0) {
|
||||
if (!simple_selectors.is_empty()) {
|
||||
callback(simple_selectors, combinator, is_rightmost);
|
||||
simple_selectors.clear();
|
||||
is_rightmost = false;
|
||||
}
|
||||
|
||||
auto const& compound_selector = compound_selectors[compound_selector_index];
|
||||
for (auto const& simple_selector : compound_selector.simple_selectors) {
|
||||
simple_selectors.append(simple_selector);
|
||||
}
|
||||
combinator = compound_selector.combinator;
|
||||
|
||||
--compound_selector_index;
|
||||
}
|
||||
if (!simple_selectors.is_empty()) {
|
||||
callback(simple_selectors, combinator, is_rightmost);
|
||||
}
|
||||
}
|
||||
|
||||
static void collect_properties_used_in_has(Selector::SimpleSelector const& selector, StyleInvalidationData& style_invalidation_data, bool in_has)
|
||||
{
|
||||
switch (selector.type) {
|
||||
case Selector::SimpleSelector::Type::Id: {
|
||||
if (in_has)
|
||||
style_invalidation_data.ids_used_in_has_selectors.set(selector.name());
|
||||
break;
|
||||
}
|
||||
case Selector::SimpleSelector::Type::Class: {
|
||||
if (in_has)
|
||||
style_invalidation_data.class_names_used_in_has_selectors.set(selector.name());
|
||||
break;
|
||||
}
|
||||
case Selector::SimpleSelector::Type::Attribute: {
|
||||
if (in_has)
|
||||
style_invalidation_data.attribute_names_used_in_has_selectors.set(selector.attribute().qualified_name.name.lowercase_name);
|
||||
break;
|
||||
}
|
||||
case Selector::SimpleSelector::Type::TagName: {
|
||||
if (in_has)
|
||||
style_invalidation_data.tag_names_used_in_has_selectors.set(selector.qualified_name().name.lowercase_name);
|
||||
break;
|
||||
}
|
||||
case Selector::SimpleSelector::Type::PseudoClass: {
|
||||
auto const& pseudo_class = selector.pseudo_class();
|
||||
for (auto const& child_selector : pseudo_class.argument_selector_list) {
|
||||
for (auto const& compound_selector : child_selector->compound_selectors()) {
|
||||
for (auto const& simple_selector : compound_selector.simple_selectors) {
|
||||
collect_properties_used_in_has(simple_selector, style_invalidation_data, in_has || pseudo_class.type == PseudoClass::Has);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
enum class ExcludePropertiesNestedInNotPseudoClass : bool {
|
||||
No,
|
||||
Yes,
|
||||
};
|
||||
|
||||
static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector const& selector, InvalidationSet& invalidation_set, ExcludePropertiesNestedInNotPseudoClass exclude_properties_nested_in_not_pseudo_class, StyleInvalidationData& rule_invalidation_data)
|
||||
{
|
||||
switch (selector.type) {
|
||||
case Selector::SimpleSelector::Type::Class:
|
||||
invalidation_set.set_needs_invalidate_class(selector.name());
|
||||
break;
|
||||
case Selector::SimpleSelector::Type::Id:
|
||||
invalidation_set.set_needs_invalidate_id(selector.name());
|
||||
break;
|
||||
case Selector::SimpleSelector::Type::TagName:
|
||||
invalidation_set.set_needs_invalidate_tag_name(selector.qualified_name().name.lowercase_name);
|
||||
break;
|
||||
case Selector::SimpleSelector::Type::Attribute:
|
||||
invalidation_set.set_needs_invalidate_attribute(selector.attribute().qualified_name.name.lowercase_name);
|
||||
break;
|
||||
case Selector::SimpleSelector::Type::PseudoClass: {
|
||||
auto const& pseudo_class = selector.pseudo_class();
|
||||
if (pseudo_class.type == PseudoClass::Has)
|
||||
break;
|
||||
if (exclude_properties_nested_in_not_pseudo_class == ExcludePropertiesNestedInNotPseudoClass::Yes && pseudo_class.type == PseudoClass::Not)
|
||||
break;
|
||||
for (auto const& nested_selector : pseudo_class.argument_selector_list) {
|
||||
auto rightmost_invalidation_set_for_selector = rule_invalidation_data.build_invalidation_sets_for_selector(*nested_selector);
|
||||
invalidation_set.include_all_from(rightmost_invalidation_set_for_selector);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
InvalidationSet StyleInvalidationData::build_invalidation_sets_for_selector(Selector const& selector)
|
||||
{
|
||||
auto const& compound_selectors = selector.compound_selectors();
|
||||
int compound_selector_index = compound_selectors.size() - 1;
|
||||
VERIFY(compound_selector_index >= 0);
|
||||
|
||||
InvalidationSet invalidation_set_for_rightmost_selector;
|
||||
Selector::Combinator previous_compound_combinator = Selector::Combinator::None;
|
||||
for_each_consecutive_simple_selector_group(selector, [&](Vector<Selector::SimpleSelector const&> const& simple_selectors, Selector::Combinator combinator, bool is_rightmost) {
|
||||
// Collect properties used in :has() so we can decide if only specific properties
|
||||
// trigger descendant invalidation or if the entire document must be invalidated.
|
||||
for (auto const& simple_selector : simple_selectors) {
|
||||
bool in_has = false;
|
||||
if (simple_selector.type == Selector::SimpleSelector::Type::PseudoClass) {
|
||||
auto const& pseudo_class = simple_selector.pseudo_class();
|
||||
if (pseudo_class.type == PseudoClass::Has)
|
||||
in_has = true;
|
||||
}
|
||||
collect_properties_used_in_has(simple_selector, *this, in_has);
|
||||
}
|
||||
|
||||
if (is_rightmost) {
|
||||
// The rightmost selector is handled twice:
|
||||
// 1) Include properties nested in :not()
|
||||
// 2) Exclude properties nested in :not()
|
||||
//
|
||||
// This ensures we handle cases like:
|
||||
// :not(.foo) => produce invalidation set .foo { $ } ($ = invalidate self)
|
||||
// .bar :not(.foo) => produce invalidation sets .foo { $ } and .bar { * } (* = invalidate subtree)
|
||||
// which means invalidation_set_for_rightmost_selector should be empty
|
||||
for (auto const& simple_selector : simple_selectors) {
|
||||
InvalidationSet s;
|
||||
build_invalidation_sets_for_simple_selector(simple_selector, s, ExcludePropertiesNestedInNotPseudoClass::No, *this);
|
||||
s.for_each_property([&](auto const& invalidation_property) {
|
||||
auto& descendant_invalidation_set = descendant_invalidation_sets.ensure(invalidation_property, [] { return InvalidationSet {}; });
|
||||
descendant_invalidation_set.set_needs_invalidate_self();
|
||||
});
|
||||
}
|
||||
|
||||
for (auto const& simple_selector : simple_selectors) {
|
||||
build_invalidation_sets_for_simple_selector(simple_selector, invalidation_set_for_rightmost_selector, ExcludePropertiesNestedInNotPseudoClass::Yes, *this);
|
||||
}
|
||||
} else {
|
||||
VERIFY(previous_compound_combinator != Selector::Combinator::None);
|
||||
for (auto const& simple_selector : simple_selectors) {
|
||||
InvalidationSet s;
|
||||
build_invalidation_sets_for_simple_selector(simple_selector, s, ExcludePropertiesNestedInNotPseudoClass::No, *this);
|
||||
s.for_each_property([&](auto const& invalidation_property) {
|
||||
auto& descendant_invalidation_set = descendant_invalidation_sets.ensure(invalidation_property, [] {
|
||||
return InvalidationSet {};
|
||||
});
|
||||
// If the rightmost selector's invalidation set is empty, it means there's no
|
||||
// specific property-based invalidation, so we fall back to invalidating the whole subtree.
|
||||
// If combinator to the right of current compound selector is NextSibling or SubsequentSibling,
|
||||
// we also need to invalidate the whole subtree, because we don't support sibling invalidation sets.
|
||||
if (AK::first_is_one_of(previous_compound_combinator, Selector::Combinator::NextSibling, Selector::Combinator::SubsequentSibling)) {
|
||||
descendant_invalidation_set.set_needs_invalidate_whole_subtree();
|
||||
} else if (invalidation_set_for_rightmost_selector.is_empty()) {
|
||||
descendant_invalidation_set.set_needs_invalidate_whole_subtree();
|
||||
} else {
|
||||
descendant_invalidation_set.include_all_from(invalidation_set_for_rightmost_selector);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
previous_compound_combinator = combinator;
|
||||
});
|
||||
|
||||
return invalidation_set_for_rightmost_selector;
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue