/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! A collection of invalidations due to changes in which stylesheets affect a //! document.
/// The kind of change that happened for a given rule. #[repr(u32)] #[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] pubenum RuleChangeKind { /// Some change in the rule which we don't know about, and could have made /// the rule change in any way.
Generic = 0, /// The rule was inserted.
Insertion, /// The rule was removed.
Removal, /// A change in the declarations of a style rule.
StyleRuleDeclarations, /// A change in the declarations of an @position-try rule.
PositionTryDeclarations,
}
/// A style sheet invalidation represents a kind of element or subtree that may /// need to be restyled. Whether it represents a whole subtree or just a single /// element is determined by the given InvalidationKind in /// StylesheetInvalidationSet's maps. #[derive(Debug, Eq, Hash, MallocSizeOf, PartialEq)] enum Invalidation { /// An element with a given id.
ID(AtomIdent), /// An element with a given class name.
Class(AtomIdent), /// An element with a given local name.
LocalName {
name: SelectorLocalName,
lower_name: SelectorLocalName,
},
}
/// Whether we should invalidate just the element, or the whole subtree within /// it. #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)] enum InvalidationKind {
None = 0,
Element,
Scope,
}
/// A set of invalidations due to stylesheet changes. /// /// TODO(emilio): We might be able to do the same analysis for media query changes too (or even /// selector changes?) specially now that we take the cascade data difference into account. #[derive(Debug, Default, MallocSizeOf)] pubstruct StylesheetInvalidationSet {
buckets: SimpleBucketsMap<InvalidationKind>,
style_fully_invalid: bool, /// The difference between the old and new cascade data, incrementally collected until flush() /// returns it. pub cascade_data_difference: CascadeDataDifference,
}
/// Mark the DOM tree styles' as fully invalid. pubfn invalidate_fully(&mutself) {
debug!("StylesheetInvalidationSet::invalidate_fully"); self.buckets.clear(); self.style_fully_invalid = true;
}
/// Analyze the given stylesheet, and collect invalidations from their rules, in order to avoid /// doing a full restyle when we style the document next time. pubfn collect_invalidations_for<S>(
&mutself,
device: &Device,
custom_media: &CustomMediaMap,
stylesheet: &S,
guard: &SharedRwLockReadGuard,
) where
S: StylesheetInDocument,
{
debug!("StylesheetInvalidationSet::collect_invalidations_for"); ifself.style_fully_invalid {
debug!(" > Fully invalid already"); return;
}
if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, custom_media, guard)
{
debug!(" > Stylesheet was not effective"); return; // Nothing to do here.
}
let quirks_mode = device.quirks_mode(); for rule in stylesheet
.contents(guard)
.effective_rules(device, custom_media, guard)
{ self.collect_invalidations_for_rule(
rule,
guard,
device,
quirks_mode, /* is_generic_change = */ false, // Note(dshin): Technically, the iterator should provide the ancestor chain as it // traverses down, but it shouldn't make a difference.
&[],
); ifself.style_fully_invalid { break;
}
}
debug!( " > resulting class invalidations: {:?}", self.buckets.classes
);
debug!(" > resulting id invalidations: {:?}", self.buckets.ids);
debug!( " > resulting local name invalidations: {:?}", self.buckets.local_names
);
debug!(" > style_fully_invalid: {}", self.style_fully_invalid);
}
/// Returns whether there's no invalidation to process. pubfn is_empty(&self) -> bool {
!self.style_fully_invalid
&& self.buckets.is_empty()
&& self.cascade_data_difference.is_empty()
}
fn invalidation_kind_for<E>(
&self,
element: E,
snapshot: Option<&Snapshot>,
quirks_mode: QuirksMode,
) -> InvalidationKind where
E: TElement,
{
debug_assert!(!self.style_fully_invalid);
letmut kind = InvalidationKind::None;
if !self.buckets.classes.is_empty() {
element.each_class(|c| {
kind.add(self.buckets.classes.get(c, quirks_mode));
});
let quirks_mode = root.as_node().owner_doc().quirks_mode(); self.process_invalidations_in_subtree(root, snapshots, quirks_mode)
}
/// Process style invalidations in a given subtree. This traverses the /// subtree looking for elements that match the invalidations in our hash /// map members. /// /// Returns whether it invalidated at least one element's style. #[allow(unsafe_code)] fn process_invalidations_in_subtree<E>(
&self,
element: E,
snapshots: Option<&SnapshotMap>,
quirks_mode: QuirksMode,
) -> bool where
E: TElement,
{
debug!("process_invalidations_in_subtree({:?})", element); letmut data = match element.mutate_data() {
Some(data) => data,
None => returnfalse,
};
if !data.has_styles() { returnfalse;
}
if data.hint.contains_subtree() {
debug!( "process_invalidations_in_subtree: {:?} was already invalid",
element
); returnfalse;
}
let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s)); let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot());
/// TODO(emilio): Reuse the bucket stuff from selectormap? That handles :is() / :where() etc. fn scan_component(
component: &Component<SelectorImpl>,
invalidation: &mut Option<Invalidation>,
) { match *component {
Component::LocalName(LocalName { ref name, ref lower_name,
}) => { if invalidation.is_none() {
*invalidation = Some(Invalidation::LocalName {
name: name.clone(),
lower_name: lower_name.clone(),
});
}
},
Component::Class(ref class) => { if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) {
*invalidation = Some(Invalidation::Class(class.clone()));
}
},
Component::ID(ref id) => { if invalidation.as_ref().map_or(true, |s| !s.is_id()) {
*invalidation = Some(Invalidation::ID(id.clone()));
}
},
_ => { // Ignore everything else, at least for now.
},
}
}
/// Collect invalidations for a given selector. /// /// We look at the outermost local name, class, or ID selector to the left /// of an ancestor combinator, in order to restyle only a given subtree. /// /// If the selector has no ancestor combinator, then we do the same for /// the only sequence it has, but record it as an element invalidation /// instead of a subtree invalidation. /// /// We prefer IDs to classs, and classes to local names, on the basis /// that the former should be more specific than the latter. We also /// prefer to generate subtree invalidations for the outermost part /// of the selector, to reduce the amount of traversal we need to do /// when flushing invalidations. fn collect_invalidations(
&mutself,
selector: &Selector<SelectorImpl>,
quirks_mode: QuirksMode,
) {
debug!( "StylesheetInvalidationSet::collect_invalidations({:?})",
selector
);
iflet Some(s) = element_invalidation {
debug!(" > Found element invalidation: {:?}", s); ifself.insert_invalidation(s, InvalidationKind::Element, quirks_mode) { return;
}
}
// The selector was of a form that we can't handle. Any element could // match it, so let's just bail out.
debug!(" > Can't handle selector or OOMd, marking fully invalid"); self.invalidate_fully()
}
fn insert_invalidation(
&mutself,
invalidation: Invalidation,
kind: InvalidationKind,
quirks_mode: QuirksMode,
) -> bool { match invalidation {
Invalidation::Class(c) => { let entry = matchself.buckets.classes.try_entry(c.0, quirks_mode) {
Ok(e) => e,
Err(..) => returnfalse,
};
*entry.or_insert(InvalidationKind::None) |= kind;
},
Invalidation::ID(i) => { let entry = matchself.buckets.ids.try_entry(i.0, quirks_mode) {
Ok(e) => e,
Err(..) => returnfalse,
};
*entry.or_insert(InvalidationKind::None) |= kind;
},
Invalidation::LocalName { name, lower_name } => { let insert_lower = name != lower_name; ifself.buckets.local_names.try_reserve(1).is_err() { returnfalse;
} let entry = self.buckets.local_names.entry(name);
*entry.or_insert(InvalidationKind::None) |= kind; if insert_lower { ifself.buckets.local_names.try_reserve(1).is_err() { returnfalse;
} let entry = self.buckets.local_names.entry(lower_name);
*entry.or_insert(InvalidationKind::None) |= kind;
}
},
}
true
}
/// Collects invalidations for a given CSS rule, if not fully invalid already. pubfn rule_changed<S>(
&mutself,
stylesheet: &S,
rule: &CssRule,
guard: &SharedRwLockReadGuard,
device: &Device,
quirks_mode: QuirksMode,
custom_media: &CustomMediaMap,
change_kind: RuleChangeKind,
ancestors: &[CssRuleRef],
) where
S: StylesheetInDocument,
{
debug!("StylesheetInvalidationSet::rule_changed"); if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, custom_media, guard)
{
debug!(" > Stylesheet was not effective"); return; // Nothing to do here.
}
if ancestors
.iter()
.any(|r| !EffectiveRules::is_effective(guard, device, quirks_mode, custom_media, r))
{
debug!(" > Ancestor rules not effective"); return;
}
if change_kind == RuleChangeKind::PositionTryDeclarations { // @position-try declaration changes need to be dealt explicitly, since the // declarations are mutable and we can't otherwise detect changes to them. match *rule {
CssRule::PositionTry(ref pt) => { self.cascade_data_difference
.changed_position_try_names
.insert(pt.read_with(guard).name.0.clone());
},
_ => debug_assert!(false, "how did position-try decls change on anything else?"),
} return;
}
ifself.style_fully_invalid { return;
}
// If the change is generic, we don't have the old rule information to know e.g., the old // media condition, or the old selector text, so we might need to invalidate more // aggressively. That only applies to the changed rules, for other rules we can just // collect invalidations as normal. let is_generic_change = change_kind == RuleChangeKind::Generic; self.collect_invalidations_for_rule(
rule,
guard,
device,
quirks_mode,
is_generic_change,
ancestors,
); ifself.style_fully_invalid { return;
}
let rules = EffectiveRulesIterator::effective_children(
device,
quirks_mode,
custom_media,
guard,
rule,
); for rule in rules { self.collect_invalidations_for_rule(
rule,
guard,
device,
quirks_mode, /* is_generic_change = */ false, // Note(dshin): Technically, the iterator should provide the ancestor chain as it traverses down, which sould be appended to `ancestors`, but it shouldn't matter.
&[],
); ifself.style_fully_invalid { break;
}
}
}
/// Collects invalidations for a given CSS rule. fn collect_invalidations_for_rule(
&mutself,
rule: &CssRule,
guard: &SharedRwLockReadGuard,
device: &Device,
quirks_mode: QuirksMode,
is_generic_change: bool,
ancestors: &[CssRuleRef],
) { usecrate::stylesheets::CssRule::*;
debug!("StylesheetInvalidationSet::collect_invalidations_for_rule");
debug_assert!(!self.style_fully_invalid, "Not worth being here!");
match *rule {
Style(ref lock) => { if is_generic_change { // TODO(emilio): We need to do this for selector / keyframe // name / font-face changes, because we don't have the old // selector / name. If we distinguish those changes // specially, then we can at least use this invalidation for // style declaration changes. returnself.invalidate_fully();
}
let style_rule = lock.read_with(guard); for selector in style_rule.selectors.slice() { self.collect_invalidations(selector, quirks_mode); ifself.style_fully_invalid { return;
}
}
},
NestedDeclarations(..) => { if ancestors.iter().any(|r| matches!(r, CssRuleRef::Scope(_))) { self.invalidate_fully();
}
},
Namespace(..) => { // It's not clear what handling changes for this correctly would // look like.
},
LayerStatement(..) => { // Layer statement insertions might alter styling order, so we need to always // invalidate fully. returnself.invalidate_fully();
},
Document(..) | Import(..) | Media(..) | Supports(..) | Container(..)
| LayerBlock(..) | StartingStyle(..) | AppearanceBase(..) => { // Do nothing, relevant nested rules are visited as part of rule iteration.
},
FontFace(..) => { // Do nothing, @font-face doesn't affect computed style information on it's own. // We'll restyle when the font face loads, if needed.
},
Page(..) | Margin(..) => { // Do nothing, we don't support OM mutations on print documents, and page rules // can't affect anything else.
},
Keyframes(ref lock) => { if is_generic_change { returnself.invalidate_fully();
} let keyframes_rule = lock.read_with(guard); if device.animation_name_may_be_referenced(&keyframes_rule.name) {
debug!( " > Found @keyframes rule potentially referenced \
from the page, marking the whole tree invalid."
); self.invalidate_fully();
} else { // Do nothing, this animation can't affect the style of existing elements.
}
},
CounterStyle(..) | Property(..) | FontFeatureValues(..) | FontPaletteValues(..) => {
debug!(" > Found unsupported rule, marking the whole subtree invalid."); self.invalidate_fully();
},
Scope(..) => { // Addition/removal of @scope requires re-evaluation of scope proximity to properly // figure out the styling order. self.invalidate_fully();
},
PositionTry(..) => { // @position-try changes doesn't change style-time information (only layout // information) and is handled by invalidate_position_try. So do nothing.
},
ViewTransition(..) => { // @view-transition doesn't affect element styles.
},
CustomMedia(..) => { // @custom-media might be referenced by other rules which we can't get a hand on in // here, so we don't know which elements are affected. // // TODO: Maybe track referenced custom-media rules like we do for @keyframe? self.invalidate_fully();
},
}
}
}
/// Invalidates for any absolutely positioned element that references the given @position-try fallback names. pubfn invalidate_position_try<E>(
element: E,
changed_names: &PrecomputedHashSet<Atom>,
invalidate_self: &mutimpl FnMut(E, &mutElementData),
invalidated_descendants: &mutimpl FnMut(E),
) -> bool where
E: TElement,
{
debug_assert!(
!changed_names.is_empty(), "Don't call me if there's nothing to do"
); letmut data = match element.mutate_data() {
Some(data) => data,
None => returnfalse,
};
letmut self_invalid = false; let style = data.styles.primary(); if style.clone_position().is_absolutely_positioned() { let fallbacks = style.clone_position_try_fallbacks(); let referenced = fallbacks.value.0.iter().any(|f| match f {
PositionTryFallbacksItem::IdentAndOrTactic(ident_or_tactic) => {
changed_names.contains(&ident_or_tactic.ident.0)
},
PositionTryFallbacksItem::PositionArea(..) => false,
});
if referenced {
self_invalid = true;
invalidate_self(element, &mut data);
}
} letmut any_children_invalid = false; for child in element.traversal_children() { let Some(e) = child.as_element() else { continue;
};
any_children_invalid |=
invalidate_position_try(e, changed_names, invalidate_self, invalidated_descendants);
} if any_children_invalid {
invalidated_descendants(element);
}
self_invalid || any_children_invalid
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.15 Sekunden
(vorverarbeitet am 2026-08-24)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.