servo: Merge #16967 - Bug 1366144: Correctly diff ::before and ::after pseudo-element styles if there's no generated content. r=heycam (from emilio:after); r=heycam,emilio
authorEmilio Cobos Álvarez <emilio@crisal.io>
Sat, 20 May 2017 13:44:31 -0500
changeset 582044 6b8c59fc67a45fe7698e12b5d5a462b3cbb7e1b7
parent 582043 7e2357039f4af46d978a03dc0adcac60f1f980d6
child 582045 2238e8f8277b756ebf66155b1ab2f431ea597b99
child 582072 61018db8d66ea9f098293fb3291cfffd013faf4c
push id59950
push userbmo:emilio+bugs@crisal.io
push dateSat, 20 May 2017 20:20:30 +0000
reviewersheycam, heycam, emilio
bugs1366144
milestone55.0a1
servo: Merge #16967 - Bug 1366144: Correctly diff ::before and ::after pseudo-element styles if there's no generated content. r=heycam (from emilio:after); r=heycam,emilio Source-Repo: https://github.com/servo/servo Source-Revision: 05a26a29967b4cb70cac0055d0a177881e047efd
servo/components/layout/animation.rs
servo/components/layout_thread/lib.rs
servo/components/script/dom/document.rs
servo/components/script/dom/element.rs
servo/components/style/data.rs
servo/components/style/gecko/generated/bindings.rs
servo/components/style/gecko/restyle_damage.rs
servo/components/style/matching.rs
servo/components/style/properties/gecko.mako.rs
servo/components/style/properties/properties.mako.rs
servo/components/style/restyle_hints.rs
servo/components/style/servo/restyle_damage.rs
servo/components/style/traversal.rs
servo/ports/geckolib/glue.rs
--- a/servo/components/layout/animation.rs
+++ b/servo/components/layout/animation.rs
@@ -155,17 +155,19 @@ pub fn recalc_style_for_animations(conte
     flow.mutate_fragments(&mut |fragment| {
         if let Some(ref animations) = animations.get(&fragment.node) {
             for animation in animations.iter() {
                 let old_style = fragment.style.clone();
                 update_style_for_animation(&context.style_context,
                                            animation,
                                            &mut fragment.style,
                                            &ServoMetricsProvider);
-                damage |= RestyleDamage::compute(&old_style, &fragment.style);
+                let difference =
+                    RestyleDamage::compute_style_difference(&old_style, &fragment.style);
+                damage |= difference.damage;
             }
         }
     });
 
     let base = flow::mut_base(flow);
     base.restyle_damage.insert(damage);
     for kid in base.children.iter_mut() {
         recalc_style_for_animations(context, kid, animations)
--- a/servo/components/layout_thread/lib.rs
+++ b/servo/components/layout_thread/lib.rs
@@ -1095,17 +1095,17 @@ impl LayoutThread {
                 let mut iter = element.as_node().traverse_preorder();
 
                 let mut next = iter.next();
                 while let Some(node) = next {
                     if node.needs_dirty_on_viewport_size_changed() {
                         let el = node.as_element().unwrap();
                         if let Some(mut d) = element.mutate_data() {
                             if d.has_styles() {
-                                d.ensure_restyle().hint.insert(&StoredRestyleHint::subtree());
+                                d.ensure_restyle().hint.insert(StoredRestyleHint::subtree());
                             }
                         }
                         if let Some(p) = el.parent_element() {
                             unsafe { p.note_dirty_descendant() };
                         }
 
                         next = iter.next_skipping_children();
                     } else {
@@ -1131,17 +1131,17 @@ impl LayoutThread {
             Some(ua_stylesheets),
             data.stylesheets_changed,
             /* author_styles_disabled = */ false,
             &mut extra_data);
         let needs_reflow = viewport_size_changed && !needs_dirtying;
         if needs_dirtying {
             if let Some(mut d) = element.mutate_data() {
                 if d.has_styles() {
-                    d.ensure_restyle().hint.insert(&StoredRestyleHint::subtree());
+                    d.ensure_restyle().hint.insert(StoredRestyleHint::subtree());
                 }
             }
         }
         if needs_reflow {
             if let Some(mut flow) = self.try_get_layout_root(element.as_node()) {
                 LayoutThread::reflow_all_nodes(FlowRef::deref_mut(&mut flow));
             }
         }
@@ -1179,17 +1179,17 @@ impl LayoutThread {
                 unsafe { el.set_has_snapshot() };
                 map.insert(el.as_node().opaque(), s);
             }
 
             let mut style_data = &mut data.base.style_data;
             let mut restyle_data = style_data.ensure_restyle();
 
             // Stash the data on the element for processing by the style system.
-            restyle_data.hint.insert(&restyle.hint.into());
+            restyle_data.hint.insert(restyle.hint.into());
             restyle_data.damage = restyle.damage;
             debug!("Noting restyle for {:?}: {:?}", el, restyle_data);
         }
 
         // Create a layout context for use throughout the following passes.
         let mut layout_context =
             self.build_layout_context(guards.clone(), true, &map);
 
--- a/servo/components/script/dom/document.rs
+++ b/servo/components/script/dom/document.rs
@@ -126,17 +126,17 @@ use std::collections::{HashMap, HashSet,
 use std::collections::hash_map::Entry::{Occupied, Vacant};
 use std::default::Default;
 use std::iter::once;
 use std::mem;
 use std::rc::Rc;
 use std::time::{Duration, Instant};
 use style::attr::AttrValue;
 use style::context::{QuirksMode, ReflowGoal};
-use style::restyle_hints::{RestyleHint, RESTYLE_SELF, RESTYLE_STYLE_ATTRIBUTE};
+use style::restyle_hints::{RestyleHint, RestyleReplacements, RESTYLE_STYLE_ATTRIBUTE};
 use style::selector_parser::{RestyleDamage, Snapshot};
 use style::shared_lock::SharedRwLock as StyleSharedRwLock;
 use style::str::{HTML_SPACE_CHARACTERS, split_html_space_chars, str_join};
 use style::stylearc::Arc;
 use style::stylesheets::Stylesheet;
 use task_source::TaskSource;
 use time;
 use timers::OneshotTimerCallback;
@@ -2356,24 +2356,24 @@ impl Document {
         // I'm getting rid of the whole hashtable soon anyway, since all it does
         // right now is populate the element restyle data in layout, and we
         // could in theory do it in the DOM I think.
         let mut entry = self.ensure_pending_restyle(el);
         if entry.snapshot.is_none() {
             entry.snapshot = Some(Snapshot::new(el.html_element_in_html_document()));
         }
         if attr.local_name() == &local_name!("style") {
-            entry.hint |= RESTYLE_STYLE_ATTRIBUTE;
+            entry.hint.insert(RestyleHint::for_replacements(RESTYLE_STYLE_ATTRIBUTE));
         }
 
         // FIXME(emilio): This should become something like
         // element.is_attribute_mapped(attr.local_name()).
         if attr.local_name() == &local_name!("width") ||
            attr.local_name() == &local_name!("height") {
-            entry.hint |= RESTYLE_SELF;
+            entry.hint.insert(RestyleHint::for_self());
         }
 
         let mut snapshot = entry.snapshot.as_mut().unwrap();
         if snapshot.attrs.is_none() {
             let attrs = el.attrs()
                           .iter()
                           .map(|attr| (attr.identifier().clone(), attr.value().clone()))
                           .collect();
--- a/servo/components/script/dom/element.rs
+++ b/servo/components/script/dom/element.rs
@@ -97,17 +97,17 @@ use std::convert::TryFrom;
 use std::default::Default;
 use std::fmt;
 use std::rc::Rc;
 use style::attr::{AttrValue, LengthOrPercentageOrAuto};
 use style::context::{QuirksMode, ReflowGoal};
 use style::element_state::*;
 use style::properties::{Importance, PropertyDeclaration, PropertyDeclarationBlock, parse_style_attribute};
 use style::properties::longhands::{self, background_image, border_spacing, font_family, font_size, overflow_x};
-use style::restyle_hints::RESTYLE_SELF;
+use style::restyle_hints::RestyleHint;
 use style::rule_tree::CascadeLevel;
 use style::selector_parser::{NonTSPseudoClass, PseudoElement, RestyleDamage, SelectorImpl, SelectorParser};
 use style::shared_lock::{SharedRwLock, Locked};
 use style::sink::Push;
 use style::stylearc::Arc;
 use style::stylist::ApplicableDeclarationBlock;
 use style::thread_state;
 use style::values::{CSSFloat, Either};
@@ -240,17 +240,17 @@ impl Element {
     }
 
     pub fn restyle(&self, damage: NodeDamage) {
         let doc = self.node.owner_doc();
         let mut restyle = doc.ensure_pending_restyle(self);
 
         // FIXME(bholley): I think we should probably only do this for
         // NodeStyleDamaged, but I'm preserving existing behavior.
-        restyle.hint |= RESTYLE_SELF;
+        restyle.hint.insert(RestyleHint::for_self());
 
         if damage == NodeDamage::OtherNodeDamage {
             restyle.damage = RestyleDamage::rebuild_and_reflow();
         }
     }
 
     // https://drafts.csswg.org/cssom-view/#css-layout-box
     // Elements that have a computed value of the display property
--- a/servo/components/style/data.rs
+++ b/servo/components/style/data.rs
@@ -5,17 +5,17 @@
 //! Per-node data used in style calculation.
 
 #![deny(missing_docs)]
 
 use context::SharedStyleContext;
 use dom::TElement;
 use properties::ComputedValues;
 use properties::longhands::display::computed_value as display;
-use restyle_hints::{RESTYLE_DESCENDANTS, RESTYLE_LATER_SIBLINGS, RESTYLE_SELF, RestyleHint};
+use restyle_hints::{RestyleReplacements, RestyleHint};
 use rule_tree::StrongRuleNode;
 use selector_parser::{EAGER_PSEUDO_COUNT, PseudoElement, RestyleDamage};
 use shared_lock::StylesheetGuards;
 #[cfg(feature = "servo")] use std::collections::HashMap;
 use std::fmt;
 #[cfg(feature = "servo")] use std::hash::BuildHasherDefault;
 use stylearc::Arc;
 use traversal::TraversalFlags;
@@ -193,75 +193,77 @@ pub struct StoredRestyleHint(RestyleHint
 impl StoredRestyleHint {
     /// Propagates this restyle hint to a child element.
     pub fn propagate(&mut self, traversal_flags: &TraversalFlags) -> Self {
         use std::mem;
 
         // In the middle of an animation only restyle, we don't need to
         // propagate any restyle hints, and we need to remove ourselves.
         if traversal_flags.for_animation_only() {
-            self.0.remove(RestyleHint::for_animations());
+            self.0.remove_animation_hints();
             return Self::empty();
         }
 
-        debug_assert!(!self.0.intersects(RestyleHint::for_animations()),
+        debug_assert!(!self.0.has_animation_hint(),
                       "There should not be any animation restyle hints \
                        during normal traversal");
 
         // Else we should clear ourselves, and return the propagated hint.
-        let hint = mem::replace(&mut self.0, RestyleHint::empty());
-        StoredRestyleHint(if hint.contains(RESTYLE_DESCENDANTS) {
-            RESTYLE_SELF | RESTYLE_DESCENDANTS
-        } else {
-            RestyleHint::empty()
-        })
+        let new_hint = mem::replace(&mut self.0, RestyleHint::empty())
+                       .propagate_for_non_animation_restyle();
+        StoredRestyleHint(new_hint)
     }
 
     /// Creates an empty `StoredRestyleHint`.
     pub fn empty() -> Self {
         StoredRestyleHint(RestyleHint::empty())
     }
 
     /// Creates a restyle hint that forces the whole subtree to be restyled,
     /// including the element.
     pub fn subtree() -> Self {
-        StoredRestyleHint(RESTYLE_SELF | RESTYLE_DESCENDANTS)
+        StoredRestyleHint(RestyleHint::subtree())
     }
 
     /// Creates a restyle hint that forces the element and all its later
     /// siblings to have their whole subtrees restyled, including the elements
     /// themselves.
     pub fn subtree_and_later_siblings() -> Self {
-        StoredRestyleHint(RESTYLE_SELF | RESTYLE_DESCENDANTS | RESTYLE_LATER_SIBLINGS)
+        StoredRestyleHint(RestyleHint::subtree_and_later_siblings())
     }
 
     /// Returns true if the hint indicates that our style may be invalidated.
     pub fn has_self_invalidations(&self) -> bool {
-        self.0.intersects(RestyleHint::for_self())
+        self.0.affects_self()
     }
 
     /// Returns true if the hint indicates that our sibling's style may be
     /// invalidated.
     pub fn has_sibling_invalidations(&self) -> bool {
-        self.0.intersects(RESTYLE_LATER_SIBLINGS)
+        self.0.affects_later_siblings()
     }
 
     /// Whether the restyle hint is empty (nothing requires to be restyled).
     pub fn is_empty(&self) -> bool {
         self.0.is_empty()
     }
 
     /// Insert another restyle hint, effectively resulting in the union of both.
-    pub fn insert(&mut self, other: &Self) {
-        self.0 |= other.0
+    pub fn insert(&mut self, other: Self) {
+        self.0.insert(other.0)
+    }
+
+    /// Insert another restyle hint, effectively resulting in the union of both.
+    pub fn insert_from(&mut self, other: &Self) {
+        self.0.insert_from(&other.0)
     }
 
     /// Returns true if the hint has animation-only restyle.
     pub fn has_animation_hint(&self) -> bool {
-        self.0.intersects(RestyleHint::for_animations())
+        self.0.has_animation_hint()
     }
 }
 
 impl Default for StoredRestyleHint {
     fn default() -> Self {
         StoredRestyleHint::empty()
     }
 }
@@ -351,17 +353,17 @@ pub struct ElementData {
 
 /// The kind of restyle that a single element should do.
 pub enum RestyleKind {
     /// We need to run selector matching plus re-cascade, that is, a full
     /// restyle.
     MatchAndCascade,
     /// We need to recascade with some replacement rule, such as the style
     /// attribute, or animation rules.
-    CascadeWithReplacements(RestyleHint),
+    CascadeWithReplacements(RestyleReplacements),
     /// We only need to recascade, for example, because only inherited
     /// properties in the parent changed.
     CascadeOnly,
 }
 
 impl ElementData {
     /// Computes the final restyle hint for this element, potentially allocating
     /// a `RestyleData` if we need to.
@@ -376,39 +378,38 @@ impl ElementData {
         context: &SharedStyleContext)
         -> bool
     {
         debug!("compute_final_hint: {:?}, {:?}",
                element,
                context.traversal_flags);
 
         let mut hint = match self.get_restyle() {
-            Some(r) => r.hint.0,
+            Some(r) => r.hint.0.clone(),
             None => RestyleHint::empty(),
         };
 
         debug!("compute_final_hint: {:?}, has_snapshot: {}, handled_snapshot: {}, \
                 pseudo: {:?}",
                 element,
                 element.has_snapshot(),
                 element.handled_snapshot(),
                 element.implemented_pseudo_element());
 
         if element.has_snapshot() && !element.handled_snapshot() {
-            hint |= context.stylist.compute_restyle_hint(&element, context.snapshot_map);
+            hint.insert(context.stylist.compute_restyle_hint(&element, context.snapshot_map));
             unsafe { element.set_handled_snapshot() }
             debug_assert!(element.handled_snapshot());
         }
 
         let empty_hint = hint.is_empty();
 
         // If the hint includes a directive for later siblings, strip it out and
         // notify the caller to modify the base hint for future siblings.
-        let later_siblings = hint.contains(RESTYLE_LATER_SIBLINGS);
-        hint.remove(RESTYLE_LATER_SIBLINGS);
+        let later_siblings = hint.remove_later_siblings_hint();
 
         // Insert the hint, overriding the previous hint. This effectively takes
         // care of removing the later siblings restyle hint.
         if !empty_hint {
             self.ensure_restyle().hint = hint.into();
         }
 
         later_siblings
@@ -440,23 +441,23 @@ impl ElementData {
                       "Should've stopped earlier");
         if !self.has_styles() {
             return RestyleKind::MatchAndCascade;
         }
 
         debug_assert!(self.restyle.is_some());
         let restyle_data = self.restyle.as_ref().unwrap();
 
-        let hint = restyle_data.hint.0;
-        if hint.contains(RESTYLE_SELF) {
+        let hint = &restyle_data.hint.0;
+        if hint.match_self() {
             return RestyleKind::MatchAndCascade;
         }
 
         if !hint.is_empty() {
-            return RestyleKind::CascadeWithReplacements(hint);
+            return RestyleKind::CascadeWithReplacements(hint.replacements);
         }
 
         debug_assert!(restyle_data.recascade,
                       "We definitely need to do something!");
         return RestyleKind::CascadeOnly;
     }
 
     /// Gets the element styles, if any.
--- a/servo/components/style/gecko/generated/bindings.rs
+++ b/servo/components/style/gecko/generated/bindings.rs
@@ -956,17 +956,18 @@ extern "C" {
      -> *mut nsStyleContext;
 }
 extern "C" {
     pub fn Gecko_GetImplementedPseudo(element: RawGeckoElementBorrowed)
      -> CSSPseudoElementType;
 }
 extern "C" {
     pub fn Gecko_CalcStyleDifference(oldstyle: *mut nsStyleContext,
-                                     newstyle: ServoComputedValuesBorrowed)
+                                     newstyle: ServoComputedValuesBorrowed,
+                                     any_style_changed: *mut bool)
      -> nsChangeHint;
 }
 extern "C" {
     pub fn Gecko_HintsHandledForDescendants(aHint: nsChangeHint)
      -> nsChangeHint;
 }
 extern "C" {
     pub fn Gecko_GetElementSnapshot(table: *const ServoElementSnapshotTable,
--- a/servo/components/style/gecko/restyle_damage.rs
+++ b/servo/components/style/gecko/restyle_damage.rs
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Gecko's restyle damage computation (aka change hints, aka `nsChangeHint`).
 
 use gecko_bindings::bindings;
 use gecko_bindings::structs;
 use gecko_bindings::structs::{nsChangeHint, nsStyleContext};
 use gecko_bindings::sugar::ownership::FFIArcHelpers;
+use matching::{StyleChange, StyleDifference};
 use properties::ComputedValues;
 use std::ops::{BitAnd, BitOr, BitOrAssign, Not};
 use stylearc::Arc;
 
 /// The representation of Gecko's restyle damage is just a wrapper over
 /// `nsChangeHint`.
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct GeckoRestyleDamage(nsChangeHint);
@@ -33,32 +34,37 @@ impl GeckoRestyleDamage {
         GeckoRestyleDamage(nsChangeHint(0))
     }
 
     /// Returns whether this restyle damage represents the empty damage.
     pub fn is_empty(&self) -> bool {
         self.0 == nsChangeHint(0)
     }
 
-    /// Computes a change hint given an old style (in the form of a
-    /// `nsStyleContext`, and a new style (in the form of `ComputedValues`).
+    /// Computes the `StyleDifference` (including the appropriate change hint)
+    /// given an old style (in the form of a `nsStyleContext`, and a new style
+    /// (in the form of `ComputedValues`).
     ///
     /// Note that we could in theory just get two `ComputedValues` here and diff
     /// them, but Gecko has an interesting optimization when they mark accessed
     /// structs, so they effectively only diff structs that have ever been
     /// accessed from layout.
-    pub fn compute(source: &nsStyleContext,
-                   new_style: &Arc<ComputedValues>) -> Self {
+    pub fn compute_style_difference(source: &nsStyleContext,
+                                    new_style: &Arc<ComputedValues>)
+                                    -> StyleDifference {
         // TODO(emilio): Const-ify this?
         let context = source as *const nsStyleContext as *mut nsStyleContext;
+        let mut any_style_changed: bool = false;
         let hint = unsafe {
             bindings::Gecko_CalcStyleDifference(context,
-                                                new_style.as_borrowed_opt().unwrap())
+                                                new_style.as_borrowed_opt().unwrap(),
+                                                &mut any_style_changed)
         };
-        GeckoRestyleDamage(hint)
+        let change = if any_style_changed { StyleChange::Changed } else { StyleChange::Unchanged };
+        StyleDifference::new(GeckoRestyleDamage(hint), change)
     }
 
     /// Returns true if this restyle damage contains all the damage of |other|.
     pub fn contains(self, other: Self) -> bool {
         self & other == other
     }
 
     /// Gets restyle damage to reconstruct the entire frame, subsuming all
--- a/servo/components/style/matching.rs
+++ b/servo/components/style/matching.rs
@@ -11,19 +11,20 @@ use Atom;
 use atomic_refcell::AtomicRefMut;
 use bit_vec::BitVec;
 use cache::{LRUCache, LRUCacheMutIterator};
 use cascade_info::CascadeInfo;
 use context::{CurrentElementInfo, SelectorFlagsMap, SharedStyleContext, StyleContext};
 use data::{ComputedStyle, ElementData, ElementStyles, RestyleData};
 use dom::{AnimationRules, SendElement, TElement, TNode};
 use font_metrics::FontMetricsProvider;
+use log::LogLevel::Trace;
 use properties::{CascadeFlags, ComputedValues, SKIP_ROOT_AND_ITEM_BASED_DISPLAY_FIXUP, cascade};
 use properties::longhands::display::computed_value as display;
-use restyle_hints::{RESTYLE_CSS_ANIMATIONS, RESTYLE_CSS_TRANSITIONS, RestyleHint};
+use restyle_hints::{RESTYLE_CSS_ANIMATIONS, RESTYLE_CSS_TRANSITIONS, RestyleReplacements};
 use restyle_hints::{RESTYLE_STYLE_ATTRIBUTE, RESTYLE_SMIL};
 use rule_tree::{CascadeLevel, RuleTree, StrongRuleNode};
 use selector_parser::{PseudoElement, RestyleDamage, SelectorImpl};
 use selectors::bloom::BloomFilter;
 use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode, StyleRelations};
 use selectors::matching::AFFECTED_BY_PSEUDO_ELEMENTS;
 use shared_lock::StylesheetGuards;
 use sink::ForgetfulSink;
@@ -46,16 +47,44 @@ enum InheritMode {
 fn relations_are_shareable(relations: &StyleRelations) -> bool {
     use selectors::matching::*;
     !relations.intersects(AFFECTED_BY_ID_SELECTOR |
                           AFFECTED_BY_PSEUDO_ELEMENTS |
                           AFFECTED_BY_STYLE_ATTRIBUTE |
                           AFFECTED_BY_PRESENTATIONAL_HINTS)
 }
 
+/// Represents the result of comparing an element's old and new style.
+pub struct StyleDifference {
+    /// The resulting damage.
+    pub damage: RestyleDamage,
+
+    /// Whether any styles changed.
+    pub change: StyleChange,
+}
+
+impl StyleDifference {
+    /// Creates a new `StyleDifference`.
+    pub fn new(damage: RestyleDamage, change: StyleChange) -> Self {
+        StyleDifference {
+            change: change,
+            damage: damage,
+        }
+    }
+}
+
+/// Represents whether or not the style of an element has changed.
+#[derive(Copy, Clone)]
+pub enum StyleChange {
+    /// The style hasn't changed.
+    Unchanged,
+    /// The style has changed.
+    Changed,
+}
+
 /// Information regarding a style sharing candidate.
 ///
 /// Note that this information is stored in TLS and cleared after the traversal,
 /// and once here, the style information of the element is immutable, so it's
 /// safe to access.
 ///
 /// TODO: We can stick a lot more info here.
 #[derive(Debug)]
@@ -370,18 +399,47 @@ impl<E: TElement> StyleSharingCandidateC
     }
 }
 
 /// The results of attempting to share a style.
 pub enum StyleSharingResult {
     /// We didn't find anybody to share the style with.
     CannotShare,
     /// The node's style can be shared. The integer specifies the index in the
-    /// LRU cache that was hit and the damage that was done.
-    StyleWasShared(usize),
+    /// LRU cache that was hit and the damage that was done. The
+    /// `ChildCascadeRequirement` indicates whether style changes due to using
+    /// the shared style mean we need to recascade to children.
+    StyleWasShared(usize, ChildCascadeRequirement),
+}
+
+/// Whether or not newly computed values for an element need to be cascade
+/// to children.
+pub enum ChildCascadeRequirement {
+    /// Old and new computed values were the same, or we otherwise know that
+    /// we won't bother recomputing style for children, so we can skip cascading
+    /// the new values into child elements.
+    CanSkipCascade,
+    /// Old and new computed values were different, so we must cascade the
+    /// new values to children.
+    ///
+    /// FIXME(heycam) Although this is "must" cascade, in the future we should
+    /// track whether child elements rely specifically on inheriting particular
+    /// property values.  When we do that, we can treat `MustCascade` as "must
+    /// cascade unless we know that changes to these properties can be
+    /// ignored".
+    MustCascade,
+}
+
+impl From<StyleChange> for ChildCascadeRequirement {
+    fn from(change: StyleChange) -> ChildCascadeRequirement {
+        match change {
+            StyleChange::Unchanged => ChildCascadeRequirement::CanSkipCascade,
+            StyleChange::Changed => ChildCascadeRequirement::MustCascade,
+        }
+    }
 }
 
 /// The result status for match primary rules.
 #[derive(Debug)]
 pub struct RulesMatchedResult {
     /// Indicate that the rule nodes are changed.
     rule_nodes_changed: bool,
     /// Indicate that there are any changes of important rules overriding animations.
@@ -565,17 +623,18 @@ trait PrivateMatchMethods: TElement {
                                 inherit_mode)
     }
 
     /// Computes values and damage for the primary or pseudo style of an element,
     /// setting them on the ElementData.
     fn cascade_primary(&self,
                        context: &mut StyleContext<Self>,
                        data: &mut ElementData,
-                       important_rules_changed: bool) {
+                       important_rules_changed: bool)
+                       -> ChildCascadeRequirement {
         // Collect some values.
         let (mut styles, restyle) = data.styles_and_restyle_mut();
         let mut primary_style = &mut styles.primary;
         let mut old_values = primary_style.values.take();
 
         // Compute the new values.
         let mut new_values = self.cascade_internal(context, primary_style, None);
 
@@ -584,51 +643,48 @@ trait PrivateMatchMethods: TElement {
         if !context.shared.traversal_flags.for_animation_only() {
             self.process_animations(context,
                                     &mut old_values,
                                     &mut new_values,
                                     primary_style,
                                     important_rules_changed);
         }
 
-        if let Some(old) = old_values {
+        let child_cascade_requirement =
             self.accumulate_damage(&context.shared,
-                                   restyle.unwrap(),
-                                   &old,
+                                   restyle,
+                                   old_values.as_ref().map(|v| v.as_ref()),
                                    &new_values,
                                    None);
-        }
 
         // Set the new computed values.
         primary_style.values = Some(new_values);
+
+        // Return whether the damage indicates we must cascade new inherited
+        // values into children.
+        child_cascade_requirement
     }
 
     fn cascade_eager_pseudo(&self,
                             context: &mut StyleContext<Self>,
                             data: &mut ElementData,
                             pseudo: &PseudoElement) {
         debug_assert!(pseudo.is_eager());
         let (mut styles, restyle) = data.styles_and_restyle_mut();
         let mut pseudo_style = styles.pseudos.get_mut(pseudo).unwrap();
         let old_values = pseudo_style.values.take();
 
         let new_values =
             self.cascade_internal(context, &styles.primary, Some(pseudo_style));
 
-        if let Some(old) = old_values {
-            // ::before and ::after are element-backed in Gecko, so they do
-            // the damage calculation for themselves.
-            if cfg!(feature = "servo") || !pseudo.is_before_or_after() {
-                self.accumulate_damage(&context.shared,
-                                       restyle.unwrap(),
-                                       &old,
-                                       &new_values,
-                                       Some(pseudo));
-            }
-        }
+        self.accumulate_damage(&context.shared,
+                               restyle,
+                               old_values.as_ref().map(|v| &**v),
+                               &new_values,
+                               Some(pseudo));
 
         pseudo_style.values = Some(new_values)
     }
 
 
     /// get_after_change_style removes the transition rules from the ComputedValues.
     /// If there is no transition rule in the ComputedValues, it returns None.
     #[cfg(feature = "gecko")]
@@ -777,64 +833,99 @@ trait PrivateMatchMethods: TElement {
                 this_opaque,
                 &**values,
                 new_values,
                 &shared_context.timer,
                 &possibly_expired_animations);
         }
     }
 
-    /// Computes and applies non-redundant damage.
-    #[cfg(feature = "gecko")]
+    /// Computes and applies restyle damage.
     fn accumulate_damage(&self,
                          shared_context: &SharedStyleContext,
-                         restyle: &mut RestyleData,
-                         old_values: &ComputedValues,
+                         restyle: Option<&mut RestyleData>,
+                         old_values: Option<&ComputedValues>,
                          new_values: &Arc<ComputedValues>,
-                         pseudo: Option<&PseudoElement>) {
+                         pseudo: Option<&PseudoElement>)
+                         -> ChildCascadeRequirement {
+        let restyle = match restyle {
+            Some(r) => r,
+            None => return ChildCascadeRequirement::MustCascade,
+        };
+
+        let old_values = match old_values {
+            Some(v) => v,
+            None => return ChildCascadeRequirement::MustCascade,
+        };
+
+        // ::before and ::after are element-backed in Gecko, so they do the
+        // damage calculation for themselves, when there's an actual pseudo.
+        let is_existing_before_or_after =
+            cfg!(feature = "gecko") &&
+            pseudo.map_or(false, |p| p.is_before_or_after()) &&
+            self.existing_style_for_restyle_damage(old_values, pseudo)
+                .is_some();
+
+        if is_existing_before_or_after {
+            return ChildCascadeRequirement::CanSkipCascade;
+        }
+
+        self.accumulate_damage_for(shared_context,
+                                   restyle,
+                                   old_values,
+                                   new_values,
+                                   pseudo)
+    }
+
+    /// Computes and applies non-redundant damage.
+    #[cfg(feature = "gecko")]
+    fn accumulate_damage_for(&self,
+                             shared_context: &SharedStyleContext,
+                             restyle: &mut RestyleData,
+                             old_values: &ComputedValues,
+                             new_values: &Arc<ComputedValues>,
+                             pseudo: Option<&PseudoElement>)
+                             -> ChildCascadeRequirement {
         // Don't accumulate damage if we're in a restyle for reconstruction.
         if shared_context.traversal_flags.for_reconstruct() {
-            return;
+            return ChildCascadeRequirement::MustCascade;
         }
 
         // If an ancestor is already getting reconstructed by Gecko's top-down
-        // frame constructor, no need to apply damage.
-        if restyle.damage_handled.contains(RestyleDamage::reconstruct()) {
-            restyle.damage = RestyleDamage::empty();
-            return;
-        }
-
-        // Add restyle damage, but only the bits that aren't redundant with respect
-        // to damage applied on our ancestors.
+        // frame constructor, no need to apply damage.  Similarly if we already
+        // have an explicitly stored ReconstructFrame hint.
         //
         // See https://bugzilla.mozilla.org/show_bug.cgi?id=1301258#c12
         // for followup work to make the optimization here more optimal by considering
         // each bit individually.
-        if !restyle.damage.contains(RestyleDamage::reconstruct()) {
-            let new_damage = self.compute_restyle_damage(&old_values,
-                                                         &new_values,
-                                                         pseudo);
-            if !restyle.damage_handled.contains(new_damage) {
-                restyle.damage |= new_damage;
-            }
+        let skip_applying_damage =
+            restyle.damage_handled.contains(RestyleDamage::reconstruct()) ||
+            restyle.damage.contains(RestyleDamage::reconstruct());
+
+        let difference = self.compute_style_difference(&old_values,
+                                                       &new_values,
+                                                       pseudo);
+        if !skip_applying_damage {
+            restyle.damage |= difference.damage;
         }
+        difference.change.into()
     }
 
     /// Computes and applies restyle damage unless we've already maxed it out.
     #[cfg(feature = "servo")]
-    fn accumulate_damage(&self,
-                         _shared_context: &SharedStyleContext,
-                         restyle: &mut RestyleData,
-                         old_values: &ComputedValues,
-                         new_values: &Arc<ComputedValues>,
-                         pseudo: Option<&PseudoElement>) {
-        if restyle.damage != RestyleDamage::rebuild_and_reflow() {
-            restyle.damage |=
-                self.compute_restyle_damage(&old_values, &new_values, pseudo);
-        }
+    fn accumulate_damage_for(&self,
+                             _shared_context: &SharedStyleContext,
+                             restyle: &mut RestyleData,
+                             old_values: &ComputedValues,
+                             new_values: &Arc<ComputedValues>,
+                             pseudo: Option<&PseudoElement>)
+                             -> ChildCascadeRequirement {
+        let difference = self.compute_style_difference(&old_values, &new_values, pseudo);
+        restyle.damage |= difference.damage;
+        difference.change.into()
     }
 
     #[cfg(feature = "servo")]
     fn update_animations_for_cascade(&self,
                                      context: &SharedStyleContext,
                                      style: &mut Arc<ComputedValues>,
                                      possibly_expired_animations: &mut Vec<::animation::PropertyAnimation>,
                                      font_metrics: &FontMetricsProvider) {
@@ -912,24 +1003,29 @@ pub enum StyleSharingBehavior {
 
 /// The public API that elements expose for selector matching.
 pub trait MatchMethods : TElement {
     /// Performs selector matching and property cascading on an element and its
     /// eager pseudos.
     fn match_and_cascade(&self,
                          context: &mut StyleContext<Self>,
                          data: &mut ElementData,
-                         sharing: StyleSharingBehavior)
+                         sharing: StyleSharingBehavior) -> ChildCascadeRequirement
     {
         // Perform selector matching for the primary style.
         let mut relations = StyleRelations::empty();
         let result = self.match_primary(context, data, &mut relations);
 
         // Cascade properties and compute primary values.
-        self.cascade_primary(context, data, result.important_rules_overriding_animation_changed);
+        let child_cascade_requirement =
+            self.cascade_primary(
+                context,
+                data,
+                result.important_rules_overriding_animation_changed
+            );
 
         // Match and cascade eager pseudo-elements.
         if !data.styles().is_display_none() {
             let _pseudo_rule_nodes_changed = self.match_pseudos(context, data);
             self.cascade_pseudos(context, data);
         }
 
         // If we have any pseudo elements, indicate so in the primary StyleRelations.
@@ -952,26 +1048,31 @@ pub trait MatchMethods : TElement {
                                                     .take();
             context.thread_local
                    .style_sharing_candidate_cache
                    .insert_if_possible(self,
                                        data.styles().primary.values(),
                                        relations,
                                        revalidation_match_results);
         }
+
+        child_cascade_requirement
     }
 
     /// Performs the cascade, without matching.
     fn cascade_primary_and_pseudos(&self,
                                    context: &mut StyleContext<Self>,
                                    mut data: &mut ElementData,
                                    important_rules_changed: bool)
+                                   -> ChildCascadeRequirement
     {
-        self.cascade_primary(context, &mut data, important_rules_changed);
+        let child_cascade_requirement =
+            self.cascade_primary(context, &mut data, important_rules_changed);
         self.cascade_pseudos(context, &mut data);
+        child_cascade_requirement
     }
 
     /// Runs selector matching to (re)compute the primary rule node for this element.
     ///
     /// Returns RulesMatchedResult which indicates whether the primary rule node changed
     /// and whether the change includes important rules.
     fn match_primary(&self,
                      context: &mut StyleContext<Self>,
@@ -1058,20 +1159,32 @@ pub trait MatchMethods : TElement {
 
         *relations = matching_context.relations;
 
         let primary_rule_node =
             compute_rule_node::<Self>(&stylist.rule_tree,
                                       &mut applicable_declarations,
                                       &context.shared.guards);
 
-        let important_rules_changed = self.has_animations() &&
-                                      data.has_styles() &&
-                                      data.important_rules_are_different(&primary_rule_node,
-                                                                         &context.shared.guards);
+        if log_enabled!(Trace) {
+            trace!("Matched rules:");
+            for rn in primary_rule_node.self_and_ancestors() {
+                if let Some(source) = rn.style_source() {
+                    trace!(" > {:?}", source);
+                }
+            }
+        }
+
+        let important_rules_changed =
+            self.has_animations() &&
+            data.has_styles() &&
+            data.important_rules_are_different(
+                &primary_rule_node,
+                &context.shared.guards
+            );
 
         RulesMatchedResult {
             rule_nodes_changed: data.set_primary_rules(primary_rule_node),
             important_rules_overriding_animation_changed: important_rules_changed,
         }
     }
 
     /// Runs selector matching to (re)compute eager pseudo-element rule nodes
@@ -1209,17 +1322,17 @@ pub trait MatchMethods : TElement {
             }
         }
     }
 
     /// Updates the rule nodes without re-running selector matching, using just
     /// the rule tree. Returns RulesChanged which indicates whether the rule nodes changed
     /// and whether the important rules changed.
     fn replace_rules(&self,
-                     hint: RestyleHint,
+                     replacements: RestyleReplacements,
                      context: &StyleContext<Self>,
                      data: &mut AtomicRefMut<ElementData>)
                      -> RulesChanged {
         use properties::PropertyDeclarationBlock;
         use shared_lock::Locked;
 
         let element_styles = &mut data.styles_mut();
         let primary_rules = &mut element_styles.primary.rules;
@@ -1241,45 +1354,45 @@ pub trait MatchMethods : TElement {
                 }
             };
 
             // Animation restyle hints are processed prior to other restyle
             // hints in the animation-only traversal.
             //
             // Non-animation restyle hints will be processed in a subsequent
             // normal traversal.
-            if hint.intersects(RestyleHint::for_animations()) {
+            if replacements.intersects(RestyleReplacements::for_animations()) {
                 debug_assert!(context.shared.traversal_flags.for_animation_only());
 
-                if hint.contains(RESTYLE_SMIL) {
+                if replacements.contains(RESTYLE_SMIL) {
                     replace_rule_node(CascadeLevel::SMILOverride,
                                       self.get_smil_override(),
                                       primary_rules);
                 }
 
                 let mut replace_rule_node_for_animation = |level: CascadeLevel,
                                                            primary_rules: &mut StrongRuleNode| {
                     let animation_rule = self.get_animation_rule_by_cascade(level);
                     replace_rule_node(level,
                                       animation_rule.as_ref(),
                                       primary_rules);
                 };
 
                 // Apply Transition rules and Animation rules if the corresponding restyle hint
                 // is contained.
-                if hint.contains(RESTYLE_CSS_TRANSITIONS) {
+                if replacements.contains(RESTYLE_CSS_TRANSITIONS) {
                     replace_rule_node_for_animation(CascadeLevel::Transitions,
                                                     primary_rules);
                 }
 
-                if hint.contains(RESTYLE_CSS_ANIMATIONS) {
+                if replacements.contains(RESTYLE_CSS_ANIMATIONS) {
                     replace_rule_node_for_animation(CascadeLevel::Animations,
                                                     primary_rules);
                 }
-            } else if hint.contains(RESTYLE_STYLE_ATTRIBUTE) {
+            } else if replacements.contains(RESTYLE_STYLE_ATTRIBUTE) {
                 let style_attribute = self.style_attribute();
                 replace_rule_node(CascadeLevel::StyleAttributeNormal,
                                   style_attribute,
                                   primary_rules);
                 replace_rule_node(CascadeLevel::StyleAttributeImportant,
                                   style_attribute,
                                   primary_rules);
             }
@@ -1337,31 +1450,32 @@ pub trait MatchMethods : TElement {
             match sharing_result {
                 Ok(shared_style) => {
                     // Yay, cache hit. Share the style.
 
                     // Accumulate restyle damage.
                     debug_assert_eq!(data.has_styles(), data.has_restyle());
                     let old_values = data.get_styles_mut()
                                          .and_then(|s| s.primary.values.take());
-                    if let Some(old) = old_values {
+                    let child_cascade_requirement =
                         self.accumulate_damage(&context.shared,
-                                               data.restyle_mut(), &old,
-                                               shared_style.values(), None);
-                    }
+                                               data.get_restyle_mut(),
+                                               old_values.as_ref().map(|v| v.as_ref()),
+                                               shared_style.values(),
+                                               None);
 
                     // We never put elements with pseudo style into the style
                     // sharing cache, so we can just mint an ElementStyles
                     // directly here.
                     //
                     // See https://bugzilla.mozilla.org/show_bug.cgi?id=1329361
                     let styles = ElementStyles::new(shared_style);
                     data.set_styles(styles);
 
-                    return StyleSharingResult::StyleWasShared(i)
+                    return StyleSharingResult::StyleWasShared(i, child_cascade_requirement)
                 }
                 Err(miss) => {
                     debug!("Cache miss: {:?}", miss);
 
                     // Cache miss, let's see what kind of failure to decide
                     // whether we keep trying or not.
                     match miss {
                         // Cache miss because of parent, clear the candidate cache.
@@ -1386,42 +1500,61 @@ pub trait MatchMethods : TElement {
         }
 
         StyleSharingResult::CannotShare
     }
 
     /// Given the old and new style of this element, and whether it's a
     /// pseudo-element, compute the restyle damage used to determine which
     /// kind of layout or painting operations we'll need.
-    fn compute_restyle_damage(&self,
-                              old_values: &ComputedValues,
-                              new_values: &Arc<ComputedValues>,
-                              pseudo: Option<&PseudoElement>)
-                              -> RestyleDamage
+    fn compute_style_difference(&self,
+                                old_values: &ComputedValues,
+                                new_values: &Arc<ComputedValues>,
+                                pseudo: Option<&PseudoElement>)
+                                -> StyleDifference
     {
-        match self.existing_style_for_restyle_damage(old_values, pseudo) {
-            Some(ref source) => RestyleDamage::compute(source, new_values),
-            None => {
-                // If there's no style source, that likely means that Gecko
-                // couldn't find a style context. This happens with display:none
-                // elements, and probably a number of other edge cases that
-                // we don't handle well yet (like display:contents).
-                if new_values.get_box().clone_display() == display::T::none &&
-                    old_values.get_box().clone_display() == display::T::none {
-                    // The style remains display:none. No need for damage.
-                    RestyleDamage::empty()
-                } else {
-                    // Something else. Be conservative for now.
-                    RestyleDamage::reconstruct()
-                }
+        if let Some(source) = self.existing_style_for_restyle_damage(old_values, pseudo) {
+            return RestyleDamage::compute_style_difference(source, new_values)
+        }
+
+        let new_style_is_display_none =
+            new_values.get_box().clone_display() == display::T::none;
+        let old_style_is_display_none =
+            old_values.get_box().clone_display() == display::T::none;
+
+        // If there's no style source, that likely means that Gecko couldn't
+        // find a style context.
+        //
+        // This happens with display:none elements, and not-yet-existing
+        // pseudo-elements.
+        if new_style_is_display_none && old_style_is_display_none {
+            // The style remains display:none. No need for damage.
+            return StyleDifference::new(RestyleDamage::empty(), StyleChange::Unchanged)
+        }
+
+        if pseudo.map_or(false, |p| p.is_before_or_after()) {
+            if (old_style_is_display_none ||
+                old_values.ineffective_content_property()) &&
+               (new_style_is_display_none ||
+                new_values.ineffective_content_property()) {
+                // The pseudo-element will remain undisplayed, so just avoid
+                // triggering any change.
+                return StyleDifference::new(RestyleDamage::empty(), StyleChange::Unchanged)
             }
+            return StyleDifference::new(RestyleDamage::reconstruct(), StyleChange::Changed)
         }
+
+        // Something else. Be conservative for now.
+        warn!("Reframing due to lack of old style source: {:?}, pseudo: {:?}",
+               self, pseudo);
+        // Something else. Be conservative for now.
+        StyleDifference::new(RestyleDamage::reconstruct(), StyleChange::Changed)
     }
 
-    /// Cascade the eager pseudo-elements of this element.
+    /// Performs the cascade for the element's eager pseudos.
     fn cascade_pseudos(&self,
                        context: &mut StyleContext<Self>,
                        mut data: &mut ElementData)
     {
         // Note that we've already set up the map of matching pseudo-elements
         // in match_pseudos (and handled the damage implications of changing
         // which pseudos match), so now we can just iterate what we have. This
         // does mean collecting owned pseudos, so that the borrow checker will
--- a/servo/components/style/properties/gecko.mako.rs
+++ b/servo/components/style/properties/gecko.mako.rs
@@ -136,16 +136,23 @@ impl ComputedValues {
         })
     }
 
     #[inline]
     pub fn is_display_contents(&self) -> bool {
         self.get_box().clone_display() == longhands::display::computed_value::T::contents
     }
 
+    /// Returns true if the value of the `content` property would make a
+    /// pseudo-element not rendered.
+    #[inline]
+    pub fn ineffective_content_property(&self) -> bool {
+        self.get_counters().ineffective_content_property()
+    }
+
     % for style_struct in data.style_structs:
     #[inline]
     pub fn clone_${style_struct.name_lower}(&self) -> Arc<style_structs::${style_struct.name}> {
         self.${style_struct.ident}.clone()
     }
     #[inline]
     pub fn get_${style_struct.name_lower}(&self) -> &style_structs::${style_struct.name} {
         &self.${style_struct.ident}
@@ -4199,16 +4206,20 @@ clip-path
     }
 
     <% impl_app_units("column_rule_width", "mColumnRuleWidth", need_clone=True,
                       round_to_pixels=True) %>
 </%self:impl_trait>
 
 <%self:impl_trait style_struct_name="Counters"
                   skip_longhands="content counter-increment counter-reset">
+    pub fn ineffective_content_property(&self) -> bool {
+        self.gecko.mContents.is_empty()
+    }
+
     pub fn set_content(&mut self, v: longhands::content::computed_value::T) {
         use properties::longhands::content::computed_value::T;
         use properties::longhands::content::computed_value::ContentItem;
         use values::generics::CounterStyleOrNone;
         use gecko_bindings::structs::nsCSSValue;
         use gecko_bindings::structs::nsStyleContentType::*;
         use gecko_bindings::bindings::Gecko_ClearAndResizeStyleContents;
 
--- a/servo/components/style/properties/properties.mako.rs
+++ b/servo/components/style/properties/properties.mako.rs
@@ -1808,16 +1808,29 @@ impl ComputedValues {
     /// Servo for obvious reasons.
     pub fn has_moz_binding(&self) -> bool { false }
 
     /// Returns whether this style's display value is equal to contents.
     ///
     /// Since this isn't supported in Servo, this is always false for Servo.
     pub fn is_display_contents(&self) -> bool { false }
 
+    #[inline]
+    /// Returns whether the "content" property for the given style is completely
+    /// ineffective, and would yield an empty `::before` or `::after`
+    /// pseudo-element.
+    pub fn ineffective_content_property(&self) -> bool {
+        use properties::longhands::content::computed_value::T;
+        match self.get_counters().content {
+            T::normal |
+            T::none => true,
+            T::Content(ref items) => items.is_empty(),
+        }
+    }
+
     /// Whether the current style is multicolumn.
     #[inline]
     pub fn is_multicol(&self) -> bool {
         let style = self.get_column();
         match style.column_width {
             Either::First(_width) => true,
             Either::Second(_auto) => match style.column_count {
                 Either::First(_n) => true,
--- a/servo/components/style/restyle_hints.rs
+++ b/servo/components/style/restyle_hints.rs
@@ -21,44 +21,48 @@ use selectors::attr::{AttrSelectorOperat
 use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode};
 use selectors::matching::matches_selector;
 use selectors::parser::{Combinator, Component, Selector, SelectorInner, SelectorMethods};
 use selectors::visitor::SelectorVisitor;
 use smallvec::SmallVec;
 use std::borrow::Borrow;
 use std::cell::Cell;
 use std::clone::Clone;
+use std::cmp;
 use stylist::SelectorMap;
 
+/// When the ElementState of an element (like IN_HOVER_STATE) changes,
+/// certain pseudo-classes (like :hover) may require us to restyle that
+/// element, its siblings, and/or its descendants. Similarly, when various
+/// attributes of an element change, we may also need to restyle things with
+/// id, class, and attribute selectors. Doing this conservatively is
+/// expensive, and so we use RestyleHints to short-circuit work we know is
+/// unnecessary.
+#[derive(Debug, Clone, PartialEq)]
+pub struct RestyleHint {
+    /// Depths at which selector matching must be re-run.
+    match_under_self: RestyleDepths,
+
+    /// Rerun selector matching on all later siblings of the element and all
+    /// of their descendants.
+    match_later_siblings: bool,
+
+    /// Levels of the cascade whose rule nodes should be recomputed and
+    /// replaced.
+    pub replacements: RestyleReplacements,
+}
+
 bitflags! {
-    /// When the ElementState of an element (like IN_HOVER_STATE) changes,
-    /// certain pseudo-classes (like :hover) may require us to restyle that
-    /// element, its siblings, and/or its descendants. Similarly, when various
-    /// attributes of an element change, we may also need to restyle things with
-    /// id, class, and attribute selectors. Doing this conservatively is
-    /// expensive, and so we use RestyleHints to short-circuit work we know is
-    /// unnecessary.
+    /// Cascade levels that can be updated for an element by simply replacing
+    /// their rule node.
     ///
     /// Note that the bit values here must be kept in sync with the Gecko
     /// nsRestyleHint values.  If you add more bits with matching values,
     /// please add assertions to assert_restyle_hints_match below.
-    pub flags RestyleHint: u32 {
-        /// Rerun selector matching on the element.
-        const RESTYLE_SELF = 0x01,
-
-        /// Rerun selector matching on all of the element's descendants.
-        ///
-        /// NB: In Gecko, we have RESTYLE_SUBTREE which is inclusive of self,
-        /// but heycam isn't aware of a good reason for that.
-        const RESTYLE_DESCENDANTS = 0x04,
-
-        /// Rerun selector matching on all later siblings of the element and all
-        /// of their descendants.
-        const RESTYLE_LATER_SIBLINGS = 0x08,
-
+    pub flags RestyleReplacements: u8 {
         /// Replace the style data coming from CSS transitions without updating
         /// any other style data. This hint is only processed in animation-only
         /// traversal which is prior to normal traversal.
         const RESTYLE_CSS_TRANSITIONS = 0x10,
 
         /// Replace the style data coming from CSS animations without updating
         /// any other style data. This hint is only processed in animation-only
         /// traversal which is prior to normal traversal.
@@ -71,84 +75,347 @@ bitflags! {
 
         /// Replace the style data coming from SMIL animations without updating
         /// any other style data. This hint is only processed in animation-only
         /// traversal which is prior to normal traversal.
         const RESTYLE_SMIL = 0x80,
     }
 }
 
-/// Asserts that all RestyleHint flags have a matching nsRestyleHint value.
+/// Eight bit wide bitfield representing depths of a DOM subtree's descendants,
+/// used to represent which elements must have selector matching re-run on them.
+///
+/// The least significant bit indicates that selector matching must be re-run
+/// for the element itself, the second least significant bit for the element's
+/// children, the third its grandchildren, and so on.  If the most significant
+/// bit it set, it indicates that that selector matching must be re-run for
+/// elements at that depth and all of their descendants.
+#[derive(Debug, Clone, Copy, PartialEq)]
+struct RestyleDepths(u8);
+
+impl RestyleDepths {
+    /// Returns a `RestyleDepths` representing no element depths.
+    fn empty() -> Self {
+        RestyleDepths(0)
+    }
+
+    /// Returns a `RestyleDepths` representing the current element depth.
+    fn for_self() -> Self {
+        RestyleDepths(0x01)
+    }
+
+    /// Returns a `RestyleDepths` representing the depths of all descendants of
+    /// the current element.
+    fn for_descendants() -> Self {
+        RestyleDepths(0xfe)
+    }
+
+    /// Returns a `RestyleDepths` representing the current element depth and the
+    /// depths of all the current element's descendants.
+    fn for_self_and_descendants() -> Self {
+        RestyleDepths(0xff)
+    }
+
+    /// Returns a `RestyleDepths` representing the specified depth, where zero
+    /// is the current element depth, one is its children's depths, etc.
+    fn for_depth(depth: u32) -> Self {
+        RestyleDepths(1u8 << cmp::min(depth, 7))
+    }
+
+    /// Returns whether this `RestyleDepths` represents the current element
+    /// depth and the depths of all the current element's descendants.
+    fn is_self_and_descendants(&self) -> bool {
+        self.0 == 0xff
+    }
+
+    /// Returns whether this `RestyleDepths` includes any element depth.
+    fn is_any(&self) -> bool {
+        self.0 != 0
+    }
+
+    /// Returns whether this `RestyleDepths` includes the current element depth.
+    fn has_self(&self) -> bool {
+        (self.0 & 0x01) != 0
+    }
+
+    /// Returns a new `RestyleDepths` with all depth values represented by this
+    /// `RestyleDepths` reduced by one.
+    fn propagate(&self) -> Self {
+        RestyleDepths((self.0 >> 1) | (self.0 & 0x80))
+    }
+
+    /// Returns a new `RestyleDepths` that represents the union of the depths
+    /// from `self` and `other`.
+    fn insert(&mut self, other: RestyleDepths) {
+        self.0 |= other.0;
+    }
+
+    /// Returns whether this `RestyleDepths` includes all depths represented
+    /// by `other`.
+    fn contains(&self, other: RestyleDepths) -> bool {
+        (self.0 & other.0) == other.0
+    }
+}
+
+/// Asserts that all RestyleReplacements have a matching nsRestyleHint value.
 #[cfg(feature = "gecko")]
 #[inline]
 pub fn assert_restyle_hints_match() {
     use gecko_bindings::structs;
 
     macro_rules! check_restyle_hints {
         ( $( $a:ident => $b:ident ),*, ) => {
             if cfg!(debug_assertions) {
-                let mut hints = RestyleHint::all();
+                let mut replacements = RestyleReplacements::all();
                 $(
                     assert_eq!(structs::$a.0 as usize, $b.bits() as usize, stringify!($b));
-                    hints.remove($b);
+                    replacements.remove($b);
                 )*
-                assert_eq!(hints, RestyleHint::empty(), "all RestyleHint bits should have an assertion");
+                assert_eq!(replacements, RestyleReplacements::empty(),
+                           "all RestyleReplacements bits should have an assertion");
             }
         }
     }
 
     check_restyle_hints! {
-        nsRestyleHint_eRestyle_Self => RESTYLE_SELF,
-        // Note that eRestyle_Subtree means "self and descendants", while
-        // RESTYLE_DESCENDANTS means descendants only.  The From impl
-        // below handles converting eRestyle_Subtree into
-        // (RESTYLE_SELF | RESTYLE_DESCENDANTS).
-        nsRestyleHint_eRestyle_Subtree => RESTYLE_DESCENDANTS,
-        nsRestyleHint_eRestyle_LaterSiblings => RESTYLE_LATER_SIBLINGS,
         nsRestyleHint_eRestyle_CSSTransitions => RESTYLE_CSS_TRANSITIONS,
         nsRestyleHint_eRestyle_CSSAnimations => RESTYLE_CSS_ANIMATIONS,
         nsRestyleHint_eRestyle_StyleAttribute => RESTYLE_STYLE_ATTRIBUTE,
         nsRestyleHint_eRestyle_StyleAttribute_Animations => RESTYLE_SMIL,
     }
 }
 
 impl RestyleHint {
-    /// The subset hints that affect the styling of a single element during the
-    /// traversal.
+    /// Creates a new, empty `RestyleHint`, which represents no restyling work
+    /// needs to be done.
+    #[inline]
+    pub fn empty() -> Self {
+        RestyleHint {
+            match_under_self: RestyleDepths::empty(),
+            match_later_siblings: false,
+            replacements: RestyleReplacements::empty(),
+        }
+    }
+
+    /// Creates a new `RestyleHint` that indicates selector matching must be
+    /// re-run on the element.
     #[inline]
     pub fn for_self() -> Self {
-        RESTYLE_SELF | RESTYLE_STYLE_ATTRIBUTE | Self::for_animations()
+        RestyleHint {
+            match_under_self: RestyleDepths::for_self(),
+            match_later_siblings: false,
+            replacements: RestyleReplacements::empty(),
+        }
+    }
+
+    /// Creates a new `RestyleHint` that indicates selector matching must be
+    /// re-run on all of the element's descendants.
+    #[inline]
+    pub fn descendants() -> Self {
+        RestyleHint {
+            match_under_self: RestyleDepths::for_descendants(),
+            match_later_siblings: false,
+            replacements: RestyleReplacements::empty(),
+        }
+    }
+
+    /// Creates a new `RestyleHint` that indicates selector matching must be
+    /// re-run on the descendants of element at the specified depth, where 0
+    /// indicates the element itself, 1 is its children, 2 its grandchildren,
+    /// etc.
+    #[inline]
+    pub fn descendants_at_depth(depth: u32) -> Self {
+        RestyleHint {
+            match_under_self: RestyleDepths::for_depth(depth),
+            match_later_siblings: false,
+            replacements: RestyleReplacements::empty(),
+        }
+    }
+
+    /// Creates a new `RestyleHint` that indicates selector matching must be
+    /// re-run on all of the element's later siblings and their descendants.
+    #[inline]
+    pub fn later_siblings() -> Self {
+        RestyleHint {
+            match_under_self: RestyleDepths::empty(),
+            match_later_siblings: true,
+            replacements: RestyleReplacements::empty(),
+        }
+    }
+
+    /// Creates a new `RestyleHint` that indicates selector matching must be
+    /// re-run on the element and all of its descendants.
+    #[inline]
+    pub fn subtree() -> Self {
+        RestyleHint {
+            match_under_self: RestyleDepths::for_self_and_descendants(),
+            match_later_siblings: false,
+            replacements: RestyleReplacements::empty(),
+        }
+    }
+
+    /// Creates a new `RestyleHint` that indicates selector matching must be
+    /// re-run on the element, its descendants, its later siblings, and
+    /// their descendants.
+    #[inline]
+    pub fn subtree_and_later_siblings() -> Self {
+        RestyleHint {
+            match_under_self: RestyleDepths::for_self_and_descendants(),
+            match_later_siblings: true,
+            replacements: RestyleReplacements::empty(),
+        }
+    }
+
+    /// Creates a new `RestyleHint` that indicates the specified rule node
+    /// replacements must be performed on the element.
+    #[inline]
+    pub fn for_replacements(replacements: RestyleReplacements) -> Self {
+        RestyleHint {
+            match_under_self: RestyleDepths::empty(),
+            match_later_siblings: false,
+            replacements: replacements,
+        }
+    }
+
+    /// Returns whether this `RestyleHint` represents no needed restyle work.
+    #[inline]
+    pub fn is_empty(&self) -> bool {
+        *self == RestyleHint::empty()
     }
 
-    /// The subset hints that are used for animation restyle.
+    /// Returns whether this `RestyleHint` represents the maximum possible
+    /// restyle work, and thus any `insert()` calls will have no effect.
+    #[inline]
+    pub fn is_maximum(&self) -> bool {
+        self.match_under_self.is_self_and_descendants() &&
+            self.match_later_siblings &&
+            self.replacements.is_all()
+    }
+
+    /// Returns whether the hint specifies that some work must be performed on
+    /// the current element.
+    #[inline]
+    pub fn affects_self(&self) -> bool {
+        self.match_self() || !self.replacements.is_empty()
+    }
+
+    /// Returns whether the hint specifies that later siblings must be restyled.
+    #[inline]
+    pub fn affects_later_siblings(&self) -> bool {
+        self.match_later_siblings
+    }
+
+    /// Returns whether the hint specifies that an animation cascade level must
+    /// be replaced.
+    #[inline]
+    pub fn has_animation_hint(&self) -> bool {
+        self.replacements.intersects(RestyleReplacements::for_animations())
+    }
+
+    /// Returns whether the hint specifies some restyle work other than an
+    /// animation cascade level replacement.
+    #[inline]
+    pub fn has_non_animation_hint(&self) -> bool {
+        self.match_under_self.is_any() || self.match_later_siblings ||
+            self.replacements.contains(RESTYLE_STYLE_ATTRIBUTE)
+    }
+
+    /// Returns whether the hint specifies that selector matching must be re-run
+    /// for the element.
+    #[inline]
+    pub fn match_self(&self) -> bool {
+        self.match_under_self.has_self()
+    }
+
+    /// Returns a new `RestyleHint` appropriate for children of the current
+    /// element.
+    #[inline]
+    pub fn propagate_for_non_animation_restyle(&self) -> Self {
+        RestyleHint {
+            match_under_self: self.match_under_self.propagate(),
+            match_later_siblings: false,
+            replacements: RestyleReplacements::empty(),
+        }
+    }
+
+    /// Removes all of the animation-related hints.
+    #[inline]
+    pub fn remove_animation_hints(&mut self) {
+        self.replacements.remove(RestyleReplacements::for_animations());
+    }
+
+    /// Removes the later siblings hint, and returns whether it was present.
+    pub fn remove_later_siblings_hint(&mut self) -> bool {
+        let later_siblings = self.match_later_siblings;
+        self.match_later_siblings = false;
+        later_siblings
+    }
+
+    /// Unions the specified `RestyleHint` into this one.
+    #[inline]
+    pub fn insert_from(&mut self, other: &Self) {
+        self.match_under_self.insert(other.match_under_self);
+        self.match_later_siblings |= other.match_later_siblings;
+        self.replacements.insert(other.replacements);
+    }
+
+    /// Unions the specified `RestyleHint` into this one.
+    #[inline]
+    pub fn insert(&mut self, other: Self) {
+        // A later patch should make it worthwhile to have an insert() function
+        // that consumes its argument.
+        self.insert_from(&other)
+    }
+
+    /// Returns whether this `RestyleHint` represents at least as much restyle
+    /// work as the specified one.
+    #[inline]
+    pub fn contains(&mut self, other: &Self) -> bool {
+        self.match_under_self.contains(other.match_under_self) &&
+        (self.match_later_siblings & other.match_later_siblings) == other.match_later_siblings &&
+        self.replacements.contains(other.replacements)
+    }
+}
+
+impl RestyleReplacements {
+    /// The replacements for the animation cascade levels.
     #[inline]
     pub fn for_animations() -> Self {
         RESTYLE_SMIL | RESTYLE_CSS_ANIMATIONS | RESTYLE_CSS_TRANSITIONS
     }
 }
 
 #[cfg(feature = "gecko")]
+impl From<nsRestyleHint> for RestyleReplacements {
+    fn from(raw: nsRestyleHint) -> Self {
+        Self::from_bits_truncate(raw.0 as u8)
+    }
+}
+
+#[cfg(feature = "gecko")]
 impl From<nsRestyleHint> for RestyleHint {
     fn from(raw: nsRestyleHint) -> Self {
-        let raw_bits: u32 = raw.0;
+        use gecko_bindings::structs::nsRestyleHint_eRestyle_LaterSiblings as eRestyle_LaterSiblings;
+        use gecko_bindings::structs::nsRestyleHint_eRestyle_Self as eRestyle_Self;
+        use gecko_bindings::structs::nsRestyleHint_eRestyle_SomeDescendants as eRestyle_SomeDescendants;
+        use gecko_bindings::structs::nsRestyleHint_eRestyle_Subtree as eRestyle_Subtree;
 
-        // FIXME(bholley): Finish aligning the binary representations here and
-        // then .expect() the result of the checked version.
-        if Self::from_bits(raw_bits).is_none() {
-            warn!("stylo: dropping unsupported restyle hint bits");
+        let mut match_under_self = RestyleDepths::empty();
+        if (raw.0 & (eRestyle_Self.0 | eRestyle_Subtree.0)) != 0 {
+            match_under_self.insert(RestyleDepths::for_self());
+        }
+        if (raw.0 & (eRestyle_Subtree.0 | eRestyle_SomeDescendants.0)) != 0 {
+            match_under_self.insert(RestyleDepths::for_descendants());
         }
 
-        let mut bits = Self::from_bits_truncate(raw_bits);
-
-        // eRestyle_Subtree converts to (RESTYLE_SELF | RESTYLE_DESCENDANTS).
-        if bits.contains(RESTYLE_DESCENDANTS) {
-            bits |= RESTYLE_SELF;
+        RestyleHint {
+            match_under_self: match_under_self,
+            match_later_siblings: (raw.0 & eRestyle_LaterSiblings.0) != 0,
+            replacements: raw.into(),
         }
-
-        bits
     }
 }
 
 #[cfg(feature = "servo")]
 impl HeapSizeOf for RestyleHint {
     fn heap_size_of_children(&self) -> usize { 0 }
 }
 
@@ -430,31 +697,16 @@ fn is_attr_selector(sel: &Component<Sele
         Component::Class(_) |
         Component::AttributeInNoNamespaceExists { .. } |
         Component::AttributeInNoNamespace { .. } |
         Component::AttributeOther(_) => true,
         _ => false,
     }
 }
 
-fn combinator_to_restyle_hint(combinator: Option<Combinator>) -> RestyleHint {
-    match combinator {
-        None => RESTYLE_SELF,
-        Some(c) => match c {
-            // NB: RESTYLE_SELF is needed to handle properly eager pseudos,
-            // otherwise we may leave a stale style on the parent.
-            Combinator::PseudoElement => RESTYLE_SELF | RESTYLE_DESCENDANTS,
-            Combinator::Child => RESTYLE_DESCENDANTS,
-            Combinator::Descendant => RESTYLE_DESCENDANTS,
-            Combinator::NextSibling => RESTYLE_LATER_SIBLINGS,
-            Combinator::LaterSibling => RESTYLE_LATER_SIBLINGS,
-        }
-    }
-}
-
 #[derive(Clone, Debug)]
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 /// The aspects of an selector which are sensitive.
 pub struct Sensitivities {
     /// The states which are sensitive.
     pub states: ElementState,
     /// Whether attributes are sensitive.
     pub attrs: bool,
@@ -574,16 +826,18 @@ pub struct DependencySet {
 }
 
 impl DependencySet {
     /// Adds a selector to this `DependencySet`.
     pub fn note_selector(&mut self, selector: &Selector<SelectorImpl>) {
         let mut combinator = None;
         let mut iter = selector.inner.complex.iter();
         let mut index = 0;
+        let mut child_combinators_seen = 0;
+        let mut saw_descendant_combinator = false;
 
         loop {
             let sequence_start = index;
             let mut visitor = SensitivitiesVisitor {
                 sensitivities: Sensitivities::new()
             };
 
             // Visit all the simple selectors in this sequence.
@@ -591,19 +845,44 @@ impl DependencySet {
             // Note that this works because we can't have combinators nested
             // inside simple selectors (i.e. in :not() or :-moz-any()). If we
             // ever support that we'll need to visit complex selectors as well.
             for ss in &mut iter {
                 ss.visit(&mut visitor);
                 index += 1; // Account for the simple selector.
             }
 
+            // Keep track of how many child combinators we've encountered,
+            // and whether we've encountered a descendant combinator at all.
+            match combinator {
+                Some(Combinator::Child) => child_combinators_seen += 1,
+                Some(Combinator::Descendant) => saw_descendant_combinator = true,
+                _ => {}
+            }
+
             // If we found a sensitivity, add an entry in the dependency set.
             if !visitor.sensitivities.is_empty() {
-                let hint = combinator_to_restyle_hint(combinator);
+                // Compute a RestyleHint given the current combinator and the
+                // tracked number of child combinators and presence of a
+                // descendant combinator.
+                let hint = match combinator {
+                    // NB: RestyleHint::subtree() and not
+                    // RestyleHint::descendants() is needed to handle properly
+                    // eager pseudos, otherwise we may leave a stale style on
+                    // the parent.
+                    Some(Combinator::PseudoElement) => RestyleHint::subtree(),
+                    Some(Combinator::Child) if !saw_descendant_combinator => {
+                        RestyleHint::descendants_at_depth(child_combinators_seen)
+                    }
+                    Some(Combinator::Child) |
+                    Some(Combinator::Descendant) => RestyleHint::descendants(),
+                    Some(Combinator::NextSibling) |
+                    Some(Combinator::LaterSibling) => RestyleHint::later_siblings(),
+                    None => RestyleHint::for_self(),
+                };
 
                 let dep_selector = if sequence_start == 0 {
                     // Reuse the bloom hashes if this is the base selector.
                     selector.inner.clone()
                 } else {
                     SelectorInner::new(selector.inner.complex.slice_from(sequence_start))
                 };
 
@@ -695,37 +974,37 @@ impl DependencySet {
             .lookup_with_additional(lookup_element, additional_id, &additional_classes, &mut |dep| {
             trace!("scanning dependency: {:?}", dep);
             if !dep.sensitivities.sensitive_to(attrs_changed,
                                                state_changes) {
                 trace!(" > non-sensitive");
                 return true;
             }
 
-            if hint.contains(dep.hint) {
+            if hint.contains(&dep.hint) {
                 trace!(" > hint was already there");
                 return true;
             }
 
             // We can ignore the selector flags, since they would have already
             // been set during original matching for any element that might
             // change its matching behavior here.
             let matched_then =
                 matches_selector(&dep.selector, &snapshot_el,
                                  &mut matching_context,
                                  &mut |_, _| {});
             let matches_now =
                 matches_selector(&dep.selector, el,
                                  &mut matching_context,
                                  &mut |_, _| {});
             if matched_then != matches_now {
-                hint.insert(dep.hint);
+                hint.insert_from(&dep.hint);
             }
 
-            !hint.is_all()
+            !hint.is_maximum()
         });
 
         debug!("Calculated restyle hint: {:?} for {:?}. (State={:?}, {} Deps)",
                hint, el, el.get_state(), self.len());
 
         hint
     }
 }
--- a/servo/components/style/servo/restyle_damage.rs
+++ b/servo/components/style/servo/restyle_damage.rs
@@ -4,16 +4,17 @@
 
 //! The restyle damage is a hint that tells layout which kind of operations may
 //! be needed in presence of incremental style changes.
 
 #![deny(missing_docs)]
 
 use computed_values::display;
 use heapsize::HeapSizeOf;
+use matching::{StyleChange, StyleDifference};
 use properties::ServoComputedValues;
 use std::fmt;
 
 bitflags! {
     #[doc = "Individual layout actions that may be necessary after restyling."]
     pub flags ServoRestyleDamage: u8 {
         #[doc = "Repaint the node itself."]
         #[doc = "Currently unused; need to decide how this propagates."]
@@ -52,22 +53,24 @@ bitflags! {
     }
 }
 
 impl HeapSizeOf for ServoRestyleDamage {
     fn heap_size_of_children(&self) -> usize { 0 }
 }
 
 impl ServoRestyleDamage {
-    /// Compute the appropriate restyle damage for a given style change between
-    /// `old` and `new`.
-    pub fn compute(old: &ServoComputedValues,
-                   new: &ServoComputedValues)
-                   -> ServoRestyleDamage {
-        compute_damage(old, new)
+    /// Compute the `StyleDifference` (including the appropriate restyle damage)
+    /// for a given style change between `old` and `new`.
+    pub fn compute_style_difference(old: &ServoComputedValues,
+                                    new: &ServoComputedValues)
+                                    -> StyleDifference {
+        let damage = compute_damage(old, new);
+        let change = if damage.is_empty() { StyleChange::Unchanged } else { StyleChange::Changed };
+        StyleDifference::new(damage, change)
     }
 
     /// Returns a bitmask that represents a flow that needs to be rebuilt and
     /// reflowed.
     ///
     /// FIXME(bholley): Do we ever actually need this? Shouldn't
     /// RECONSTRUCT_FLOW imply everything else?
     pub fn rebuild_and_reflow() -> ServoRestyleDamage {
--- a/servo/components/style/traversal.rs
+++ b/servo/components/style/traversal.rs
@@ -3,18 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Traversing the DOM tree; the bloom filter.
 
 use atomic_refcell::{AtomicRefCell, AtomicRefMut};
 use context::{SharedStyleContext, StyleContext, ThreadLocalStyleContext};
 use data::{ElementData, ElementStyles, StoredRestyleHint};
 use dom::{DirtyDescendants, NodeInfo, OpaqueNode, TElement, TNode};
-use matching::{MatchMethods, StyleSharingBehavior};
-use restyle_hints::{RESTYLE_DESCENDANTS, RESTYLE_SELF};
+use matching::{ChildCascadeRequirement, MatchMethods, StyleSharingBehavior};
+use restyle_hints::RestyleHint;
 use selector_parser::RestyleDamage;
 #[cfg(feature = "servo")] use servo_config::opts;
 use std::borrow::BorrowMut;
 
 /// A per-traversal-level chunk of data. This is sent down by the traversal, and
 /// currently only holds the dom depth for the bloom filter.
 ///
 /// NB: Keep this as small as possible, please!
@@ -227,17 +227,17 @@ pub trait DomTraversal<E: TElement> : Sy
         // Expanding snapshots here may create a LATER_SIBLINGS restyle hint, which
         // we propagate to the next sibling element.
         if let Some(mut data) = root.mutate_data() {
             let later_siblings = data.compute_final_hint(root, shared_context);
             if later_siblings {
                 if let Some(next) = root.next_sibling_element() {
                     if let Some(mut next_data) = next.mutate_data() {
                         let hint = StoredRestyleHint::subtree_and_later_siblings();
-                        next_data.ensure_restyle().hint.insert(&hint);
+                        next_data.ensure_restyle().hint.insert(hint);
                     }
                 }
             }
         }
 
         PreTraverseToken {
             traverse: Self::node_needs_traversal(root.as_node(), traversal_flags),
             unstyled_children_only: false,
@@ -606,28 +606,30 @@ pub fn recalc_style_at<E, D>(traversal: 
     let compute_self = !element.has_current_styles(data);
     let mut inherited_style_changed = false;
 
     debug!("recalc_style_at: {:?} (compute_self={:?}, dirty_descendants={:?}, data={:?})",
            element, compute_self, element.has_dirty_descendants(), data);
 
     // Compute style for this element if necessary.
     if compute_self {
-        compute_style(traversal, traversal_data, context, element, &mut data);
+        match compute_style(traversal, traversal_data, context, element, &mut data) {
+            ChildCascadeRequirement::MustCascade => {
+                inherited_style_changed = true;
+            }
+            ChildCascadeRequirement::CanSkipCascade => {}
+        };
 
         // If we're restyling this element to display:none, throw away all style
         // data in the subtree, notify the caller to early-return.
         if data.styles().is_display_none() {
             debug!("{:?} style is display:none - clearing data from descendants.",
                    element);
             clear_descendant_data(element, &|e| unsafe { D::clear_element_data(&e) });
         }
-
-        // FIXME(bholley): Compute this accurately from the call to CalcStyleDifference.
-        inherited_style_changed = true;
     }
 
     // Now that matching and cascading is done, clear the bits corresponding to
     // those operations and compute the propagated restyle hint.
     let propagated_hint = match data.get_restyle_mut() {
         None => StoredRestyleHint::empty(),
         Some(r) => {
             debug_assert!(context.shared.traversal_flags.for_animation_only() ||
@@ -706,60 +708,71 @@ pub fn recalc_style_at<E, D>(traversal: 
         unsafe { element.unset_dirty_descendants(); }
     }
 }
 
 fn compute_style<E, D>(_traversal: &D,
                        traversal_data: &PerLevelTraversalData,
                        context: &mut StyleContext<E>,
                        element: E,
-                       mut data: &mut AtomicRefMut<ElementData>)
+                       mut data: &mut AtomicRefMut<ElementData>) -> ChildCascadeRequirement
     where E: TElement,
           D: DomTraversal<E>,
 {
     use data::RestyleKind::*;
     use matching::StyleSharingResult::*;
 
     context.thread_local.statistics.elements_styled += 1;
     let kind = data.restyle_kind();
 
     // First, try the style sharing cache. If we get a match we can skip the rest
     // of the work.
     if let MatchAndCascade = kind {
         let sharing_result = unsafe {
             element.share_style_if_possible(context, &mut data)
         };
-        if let StyleWasShared(index) = sharing_result {
+        if let StyleWasShared(index, had_damage) = sharing_result {
             context.thread_local.statistics.styles_shared += 1;
             context.thread_local.style_sharing_candidate_cache.touch(index);
-            return;
+            return had_damage;
         }
     }
 
     match kind {
         MatchAndCascade => {
             // Ensure the bloom filter is up to date.
             context.thread_local.bloom_filter
                    .insert_parents_recovering(element, traversal_data.current_dom_depth);
 
             context.thread_local.bloom_filter.assert_complete(element);
             context.thread_local.statistics.elements_matched += 1;
 
             // Perform the matching and cascading.
-            element.match_and_cascade(context, &mut data, StyleSharingBehavior::Allow);
+            element.match_and_cascade(
+                context,
+                &mut data,
+                StyleSharingBehavior::Allow
+            )
         }
-        CascadeWithReplacements(hint) => {
-            let rules_changed = element.replace_rules(hint, context, &mut data);
-            element.cascade_primary_and_pseudos(context, &mut data,
-                                                rules_changed.important_rules_changed());
+        CascadeWithReplacements(flags) => {
+            let rules_changed = element.replace_rules(flags, context, &mut data);
+            element.cascade_primary_and_pseudos(
+                context,
+                &mut data,
+                rules_changed.important_rules_changed()
+            )
         }
         CascadeOnly => {
-            element.cascade_primary_and_pseudos(context, &mut data, false);
+            element.cascade_primary_and_pseudos(
+                context,
+                &mut data,
+                /* important_rules_changed = */ false
+            )
         }
-    };
+    }
 }
 
 fn preprocess_children<E, D>(traversal: &D,
                              element: E,
                              mut propagated_hint: StoredRestyleHint,
                              damage_handled: RestyleDamage,
                              parent_inherited_style_changed: bool)
     where E: TElement,
@@ -801,21 +814,21 @@ fn preprocess_children<E, D>(traversal: 
            damage_handled.is_empty() && !child_data.has_restyle() {
             continue;
         }
 
         let mut restyle_data = child_data.ensure_restyle();
 
         // Propagate the parent and sibling restyle hint.
         if !propagated_hint.is_empty() {
-            restyle_data.hint.insert(&propagated_hint);
+            restyle_data.hint.insert_from(&propagated_hint);
         }
 
         if later_siblings {
-            propagated_hint.insert(&(RESTYLE_SELF | RESTYLE_DESCENDANTS).into());
+            propagated_hint.insert(RestyleHint::subtree().into());
         }
 
         // Store the damage already handled by ancestors.
         restyle_data.set_damage_handled(damage_handled);
 
         // If properties that we inherited from the parent changed, we need to
         // recascade.
         //
--- a/servo/ports/geckolib/glue.rs
+++ b/servo/ports/geckolib/glue.rs
@@ -2111,26 +2111,26 @@ pub extern "C" fn Servo_NoteExplicitHint
                                           restyle_hint: nsRestyleHint,
                                           change_hint: nsChangeHint) {
     let element = GeckoElement(element);
     let damage = GeckoRestyleDamage::new(change_hint);
     debug!("Servo_NoteExplicitHints: {:?}, restyle_hint={:?}, change_hint={:?}",
            element, restyle_hint, change_hint);
 
     let restyle_hint: RestyleHint = restyle_hint.into();
-    debug_assert!(RestyleHint::for_animations().contains(restyle_hint) ||
-                  !RestyleHint::for_animations().intersects(restyle_hint),
+    debug_assert!(!(restyle_hint.has_animation_hint() &&
+                    restyle_hint.has_non_animation_hint()),
                   "Animation restyle hints should not appear with non-animation restyle hints");
 
     let mut maybe_data = element.mutate_data();
     let maybe_restyle_data = maybe_data.as_mut().and_then(|d| unsafe {
-        maybe_restyle(d, element, restyle_hint.intersects(RestyleHint::for_animations()))
+        maybe_restyle(d, element, restyle_hint.has_animation_hint())
     });
     if let Some(restyle_data) = maybe_restyle_data {
-        restyle_data.hint.insert(&restyle_hint.into());
+        restyle_data.hint.insert(restyle_hint.into());
         restyle_data.damage |= damage;
     } else {
         debug!("(Element not styled, discarding hints)");
     }
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_ImportRule_GetSheet(import_rule: