LibWeb: Avoid media rule reevaluation for matchMedia

Separate MediaQueryList change reporting from stylesheet media rule
invalidation. Creating matchMedia() objects evaluates their own baseline
state, but should not make the next style update walk all active
stylesheets when the media environment has not changed.

This avoids continuous stylesheet media query reevaluation during
YouTube video playback, where repeated matchMedia() creation can make
style flushes do unnecessary work.
This commit is contained in:
Andreas Kling 2026-06-12 17:46:21 +02:00 committed by Andreas Kling
parent 2c33de1583
commit 49730156ae
6 changed files with 81 additions and 37 deletions

View file

@ -44,6 +44,8 @@ static void invalidate_style_after_media_rule_changes(DOM::Node& root, MediaQuer
void evaluate_media_rules_and_invalidate_style(DOM::Document& document)
{
++document.style_invalidation_counters().media_rule_evaluations;
bool document_media_queries_changed_match_state = false;
MediaQueryRuleInvalidation document_invalidation;
document.style_scope().for_each_active_css_style_sheet([&](CSS::CSSStyleSheet& style_sheet) {

View file

@ -237,7 +237,7 @@ static Optional<u64> s_style_invalidation_counter_dump_interval;
static void dump_style_invalidation_counters(Document const& document)
{
auto const& counters = document.style_invalidation_counters();
dbgln("Style invalidation counters for {}: styleInvalidations={}, fullStyleInvalidations={}, elementStyleRecomputations={}, elementStyleNoopRecomputations={}, elementInheritedStyleRecomputations={}, elementInheritedStyleNoopRecomputations={}, previousSiblingInvalidationWalkVisits={}, hasAncestorWalkInvocations={}, hasAncestorWalkVisits={}, hasAncestorSiblingElementChecks={}, hasInvalidationMetadataCandidates={}, hasMatchInvocations={}, hasResultCacheHits={}, hasResultCacheMisses={}",
dbgln("Style invalidation counters for {}: styleInvalidations={}, fullStyleInvalidations={}, elementStyleRecomputations={}, elementStyleNoopRecomputations={}, elementInheritedStyleRecomputations={}, elementInheritedStyleNoopRecomputations={}, previousSiblingInvalidationWalkVisits={}, mediaRuleEvaluations={}, hasAncestorWalkInvocations={}, hasAncestorWalkVisits={}, hasAncestorSiblingElementChecks={}, hasInvalidationMetadataCandidates={}, hasMatchInvocations={}, hasResultCacheHits={}, hasResultCacheMisses={}",
document.url_string(),
counters.style_invalidations,
counters.full_style_invalidations,
@ -246,6 +246,7 @@ static void dump_style_invalidation_counters(Document const& document)
counters.element_inherited_style_recomputations,
counters.element_inherited_style_noop_recomputations,
counters.previous_sibling_invalidation_walk_visits,
counters.media_rule_evaluations,
counters.has_ancestor_walk_invocations,
counters.has_ancestor_walk_visits,
counters.has_ancestor_sibling_element_checks,
@ -2201,14 +2202,14 @@ void Document::update_style()
CSS::Invalidation::invalidate_style_for_pending_has_mutations(*this);
}
if (!m_style_invalidator->has_pending_invalidations() && !needs_full_style_update() && !needs_style_update() && !child_needs_style_update() && !m_needs_media_query_evaluation)
if (!m_style_invalidator->has_pending_invalidations() && !needs_full_style_update() && !needs_style_update() && !child_needs_style_update() && !m_needs_media_rule_evaluation)
return;
// NOTE: If this is a document hosting <template> contents, style update is unnecessary.
if (m_created_for_appropriate_template_contents)
return;
if (m_needs_media_query_evaluation)
if (m_needs_media_rule_evaluation)
evaluate_media_rules();
if (!m_style_invalidator->has_pending_invalidations() && !needs_full_style_update() && !needs_style_update() && !child_needs_style_update())
@ -2327,7 +2328,7 @@ CSS::ComputedProperties const* Document::update_style_for_element(AbstractElemen
// Media query evaluation can enqueue normal style invalidations, so do it before deciding whether the full
// style traversal needs to run.
if (m_needs_media_query_evaluation)
if (m_needs_media_rule_evaluation)
evaluate_media_rules();
if (!m_is_running_update_layout
@ -2451,7 +2452,7 @@ bool Document::element_needs_style_update(AbstractElement const& abstract_elemen
return true;
if (m_needs_invalidation_of_elements_affected_by_has)
return true;
if (m_needs_media_query_evaluation)
if (m_needs_media_rule_evaluation)
return true;
if (m_style_invalidator->has_pending_invalidations())
return true;
@ -4671,55 +4672,61 @@ void Document::run_the_scroll_steps()
void Document::add_media_query_list(GC::Ref<CSS::MediaQueryList> media_query_list)
{
m_needs_media_query_evaluation = true;
m_media_query_lists.append(media_query_list);
m_needs_media_query_list_evaluation = true;
}
// https://drafts.csswg.org/cssom-view/#evaluate-media-queries-and-report-changes
void Document::evaluate_media_queries_and_report_changes()
{
if (!m_needs_media_query_evaluation)
if (!m_needs_media_query_list_evaluation && !m_needs_media_rule_evaluation)
return;
m_needs_media_query_evaluation = false;
// NOTE: Not in the spec, but we take this opportunity to prune null WeakPtrs.
m_media_query_lists.remove_all_matching([](auto& it) {
return !it;
});
bool evaluate_media_query_lists = m_needs_media_query_list_evaluation;
m_needs_media_query_list_evaluation = false;
// 1. For each MediaQueryList object target that has doc as its document,
// in the order they were created, oldest first, run these substeps:
for (auto& media_query_list_ptr : m_media_query_lists) {
// 1. If targets matches state has changed since the last time these steps
// were run, fire an event at target using the MediaQueryListEvent constructor,
// with its type attribute initialized to change, its isTrusted attribute
// initialized to true, its media attribute initialized to targets media,
// and its matches attribute initialized to targets matches state.
if (!media_query_list_ptr)
continue;
GC::Ptr<CSS::MediaQueryList> media_query_list = media_query_list_ptr.ptr();
bool did_match = media_query_list->matches();
bool now_matches = media_query_list->evaluate();
if (evaluate_media_query_lists) {
// NOTE: Not in the spec, but we take this opportunity to prune null WeakPtrs.
m_media_query_lists.remove_all_matching([](auto& it) {
return !it;
});
auto did_change_internally = media_query_list->has_changed_state();
media_query_list->set_has_changed_state(false);
// 1. For each MediaQueryList object target that has doc as its document,
// in the order they were created, oldest first, run these substeps:
for (auto& media_query_list_ptr : m_media_query_lists) {
// 1. If targets matches state has changed since the last time these steps
// were run, fire an event at target using the MediaQueryListEvent constructor,
// with its type attribute initialized to change, its isTrusted attribute
// initialized to true, its media attribute initialized to targets media,
// and its matches attribute initialized to targets matches state.
if (!media_query_list_ptr)
continue;
GC::Ptr<CSS::MediaQueryList> media_query_list = media_query_list_ptr.ptr();
bool did_match = media_query_list->matches();
bool now_matches = media_query_list->evaluate();
if (did_change_internally == true || did_match != now_matches) {
Bindings::MediaQueryListEventInit init;
init.media = media_query_list->media();
init.matches = now_matches;
auto event = CSS::MediaQueryListEvent::create(realm(), HTML::EventNames::change, init);
event->set_is_trusted(true);
media_query_list->dispatch_event(*event);
auto did_change_internally = media_query_list->has_changed_state();
media_query_list->set_has_changed_state(false);
if (did_change_internally == true || did_match != now_matches) {
Bindings::MediaQueryListEventInit init;
init.media = media_query_list->media();
init.matches = now_matches;
auto event = CSS::MediaQueryListEvent::create(realm(), HTML::EventNames::change, init);
event->set_is_trusted(true);
media_query_list->dispatch_event(*event);
}
}
}
// Also not in the spec, but this is as good a place as any to evaluate @media rules!
evaluate_media_rules();
if (m_needs_media_rule_evaluation)
evaluate_media_rules();
}
void Document::evaluate_media_rules()
{
m_needs_media_rule_evaluation = false;
CSS::Invalidation::evaluate_media_rules_and_invalidate_style(*this);
}

View file

@ -629,7 +629,11 @@ public:
void run_the_scroll_steps();
void evaluate_media_queries_and_report_changes();
void set_needs_media_query_evaluation() { m_needs_media_query_evaluation = true; }
void set_needs_media_query_evaluation()
{
m_needs_media_query_list_evaluation = true;
m_needs_media_rule_evaluation = true;
}
void add_media_query_list(GC::Ref<CSS::MediaQueryList>);
GC::Ref<CSS::VisualViewport> visual_viewport();
@ -907,6 +911,7 @@ public:
u64 element_inherited_style_noop_recomputations { 0 };
u64 previous_sibling_invalidation_walk_visits { 0 };
u64 descendant_slot_invalidation_subtree_scans { 0 };
u64 media_rule_evaluations { 0 };
};
StyleInvalidationCounters& style_invalidation_counters() const { return m_style_invalidation_counters; }
void reset_style_invalidation_counters() const;
@ -1364,7 +1369,8 @@ private:
Vector<PendingScrollEvent> m_pending_scroll_events;
// Used by evaluate_media_queries_and_report_changes().
bool m_needs_media_query_evaluation { false };
bool m_needs_media_query_list_evaluation { false };
bool m_needs_media_rule_evaluation { false };
Vector<GC::Weak<CSS::MediaQueryList>> m_media_query_lists;
bool m_needs_full_style_update { false };

View file

@ -751,6 +751,7 @@ JS::Object* Internals::get_style_invalidation_counters()
object->define_direct_property("elementInheritedStyleNoopRecomputations"_utf16_fly_string, JS::Value(counters.element_inherited_style_noop_recomputations), JS::default_attributes);
object->define_direct_property("previousSiblingInvalidationWalkVisits"_utf16_fly_string, JS::Value(counters.previous_sibling_invalidation_walk_visits), JS::default_attributes);
object->define_direct_property("descendantSlotInvalidationSubtreeScans"_utf16_fly_string, JS::Value(counters.descendant_slot_invalidation_subtree_scans), JS::default_attributes);
object->define_direct_property("mediaRuleEvaluations"_utf16_fly_string, JS::Value(counters.media_rule_evaluations), JS::default_attributes);
return object;
}

View file

@ -0,0 +1 @@
media rule evaluations after matchMedia: 0

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script src="../../include.js"></script>
<script>
test(() => {
const style = document.createElement("style");
style.textContent = `
@media (min-width: 1px) {
body { color: rgb(1, 2, 3); }
}
`;
document.head.appendChild(style);
document.body.offsetWidth;
internals.resetStyleInvalidationCounters();
for (let i = 0; i < 100; ++i) {
const mediaQueryList = matchMedia("(min-width: 1px)");
if (!mediaQueryList.matches)
throw new Error("MediaQueryList should match current viewport");
internals.updateStyle();
}
const counters = internals.getStyleInvalidationCounters();
println(`media rule evaluations after matchMedia: ${counters.mediaRuleEvaluations}`);
});
</script>