Bug 1646811 - servo: Cache animation computed values when animations change.
authorMartin Robinson <mrobinson@igalia.com>
Thu, 18 Jun 2020 18:11:57 +0000
changeset 536345 400228984077938439d5e7520341ca3a44e732ea
parent 536344 1743d20e0d029ea47bb57884c678afae4c15b953
child 536346 f6e4935b184b705639042d91e25042807c717c8d
push id37520
push userdluca@mozilla.com
push dateFri, 19 Jun 2020 04:04:08 +0000
treeherdermozilla-central@d1a4f9157858 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1646811
milestone79.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1646811 - servo: Cache animation computed values when animations change. Instead of recalculating the animation style every tick of an animation, cache the computed values when animations change. In addition to being more efficient, this will allow us to return animation rules as property declarations because we don't need to consult the final style to produce them. Depends on D80232 Differential Revision: https://phabricator.services.mozilla.com/D80233
servo/components/style/animation.rs
servo/components/style/dom.rs
servo/components/style/gecko/wrapper.rs
servo/components/style/matching.rs
servo/components/style/properties/properties.mako.rs
--- a/servo/components/style/animation.rs
+++ b/servo/components/style/animation.rs
@@ -12,17 +12,17 @@ use crate::context::SharedStyleContext;
 use crate::dom::{OpaqueNode, TElement, TNode};
 use crate::font_metrics::FontMetricsProvider;
 use crate::properties::animated_properties::AnimationValue;
 use crate::properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection;
 use crate::properties::longhands::animation_fill_mode::computed_value::single_value::T as AnimationFillMode;
 use crate::properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState;
 use crate::properties::LonghandIdSet;
 use crate::properties::{self, CascadeMode, ComputedValues, LonghandId};
-use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue};
+use crate::stylesheets::keyframes_rule::{KeyframesStep, KeyframesStepValue};
 use crate::stylesheets::Origin;
 use crate::values::animated::{Animate, Procedure};
 use crate::values::computed::Time;
 use crate::values::computed::TimingFunction;
 use crate::values::generics::box_::AnimationIterationCount;
 use crate::values::generics::easing::{StepPosition, TimingFunction as GenericTimingFunction};
 use crate::Atom;
 use servo_arc::Arc;
@@ -167,27 +167,127 @@ impl AnimationState {
 #[derive(Clone, Debug, MallocSizeOf)]
 pub enum KeyframesIterationState {
     /// Infinite iterations with the current iteration count.
     Infinite(f64),
     /// Current and max iterations.
     Finite(f64, f64),
 }
 
+/// A single computed keyframe for a CSS Animation.
+#[derive(Clone, MallocSizeOf)]
+struct ComputedKeyframeStep {
+    step: KeyframesStep,
+
+    #[ignore_malloc_size_of = "ComputedValues"]
+    style: Arc<ComputedValues>,
+
+    timing_function: TimingFunction,
+}
+
+impl ComputedKeyframeStep {
+    fn generate_for_keyframes<E>(
+        element: E,
+        steps: &[KeyframesStep],
+        context: &SharedStyleContext,
+        base_style: &Arc<ComputedValues>,
+        font_metrics_provider: &dyn FontMetricsProvider,
+        default_timing_function: TimingFunction,
+    ) -> Vec<Self>
+    where
+        E: TElement,
+    {
+        let mut previous_style = base_style.clone();
+        steps
+            .iter()
+            .cloned()
+            .map(|step| match step.value {
+                KeyframesStepValue::ComputedValues => ComputedKeyframeStep {
+                    step,
+                    style: base_style.clone(),
+                    timing_function: default_timing_function,
+                },
+                KeyframesStepValue::Declarations {
+                    block: ref declarations,
+                } => {
+                    let guard = declarations.read_with(context.guards.author);
+
+                    let iter = || {
+                        // It's possible to have !important properties in keyframes
+                        // so we have to filter them out.
+                        // See the spec issue https://github.com/w3c/csswg-drafts/issues/1824
+                        // Also we filter our non-animatable properties.
+                        guard
+                            .normal_declaration_iter()
+                            .filter(|declaration| declaration.is_animatable())
+                            .map(|decl| (decl, Origin::Author))
+                    };
+
+                    // This currently ignores visited styles, which seems acceptable,
+                    // as existing browsers don't appear to animate visited styles.
+                    //
+                    // TODO(mrobinson): We shouldn't be calling `apply_declarations`
+                    // here because it doesn't really produce the correct values (for
+                    // instance for keyframes that are missing animating properties).
+                    // Instead we should do something like what Gecko does in
+                    // Servo_StyleSet_GetKeyframesForName.
+                    let computed_style = properties::apply_declarations::<E, _, _>(
+                        context.stylist.device(),
+                        /* pseudo = */ None,
+                        previous_style.rules(),
+                        &context.guards,
+                        iter,
+                        Some(&previous_style),
+                        Some(&previous_style),
+                        Some(&previous_style),
+                        font_metrics_provider,
+                        CascadeMode::Unvisited {
+                            visited_rules: None,
+                        },
+                        context.quirks_mode(),
+                        /* rule_cache = */ None,
+                        &mut Default::default(),
+                        Some(element),
+                    );
+
+                    // NB: The spec says that the timing function can be overwritten
+                    // from the keyframe style. `animation_timing_function` can never
+                    // be empty, always has at least the default value (`ease`).
+                    let timing_function = if step.declared_timing_function {
+                        computed_style.get_box().animation_timing_function_at(0)
+                    } else {
+                        default_timing_function
+                    };
+
+                    previous_style = computed_style.clone();
+                    ComputedKeyframeStep {
+                        step,
+                        style: computed_style,
+                        timing_function,
+                    }
+                },
+            })
+            .collect()
+    }
+}
+
 /// A CSS Animation
 #[derive(Clone, MallocSizeOf)]
 pub struct Animation {
     /// The node associated with this animation.
     pub node: OpaqueNode,
 
     /// The name of this animation as defined by the style.
     pub name: Atom,
 
-    /// The internal animation from the style system.
-    pub keyframes_animation: KeyframesAnimation,
+    /// The properties that change in this animation.
+    properties_changed: LonghandIdSet,
+
+    /// The computed style for each keyframe of this animation.
+    computed_steps: Vec<ComputedKeyframeStep>,
 
     /// The time this animation started at, which is the current value of the animation
     /// timeline when this animation was created plus any animation delay.
     pub started_at: f64,
 
     /// The duration of this animation.
     pub duration: f64,
 
@@ -372,36 +472,29 @@ impl Animation {
         // avoid sending multiple animationstart events.
         if self.state == Pending && self.started_at <= now && old_state != Pending {
             self.state = Running;
         }
     }
 
     /// Update the given style to reflect the values specified by this `Animation`
     /// at the time provided by the given `SharedStyleContext`.
-    fn update_style<E>(
-        &self,
-        context: &SharedStyleContext,
-        style: &mut Arc<ComputedValues>,
-        font_metrics_provider: &dyn FontMetricsProvider,
-    ) where
-        E: TElement,
-    {
+    fn update_style(&self, context: &SharedStyleContext, style: &mut Arc<ComputedValues>) {
         let duration = self.duration;
         let started_at = self.started_at;
 
         let now = match self.state {
             AnimationState::Running | AnimationState::Pending | AnimationState::Finished => {
                 context.current_time_for_animations
             },
             AnimationState::Paused(progress) => started_at + duration * progress,
             AnimationState::Canceled => return,
         };
 
-        debug_assert!(!self.keyframes_animation.steps.is_empty());
+        debug_assert!(!self.computed_steps.is_empty());
 
         let mut total_progress = (now - started_at) / duration;
         if total_progress < 0. &&
             self.fill_mode != AnimationFillMode::Backwards &&
             self.fill_mode != AnimationFillMode::Both
         {
             return;
         }
@@ -412,149 +505,110 @@ impl Animation {
         {
             return;
         }
         total_progress = total_progress.min(1.0).max(0.0);
 
         // Get the indices of the previous (from) keyframe and the next (to) keyframe.
         let next_keyframe_index;
         let prev_keyframe_index;
+        let num_steps = self.computed_steps.len();
+        debug_assert!(num_steps > 0);
         match self.current_direction {
             AnimationDirection::Normal => {
                 next_keyframe_index = self
-                    .keyframes_animation
-                    .steps
+                    .computed_steps
                     .iter()
-                    .position(|step| total_progress as f32 <= step.start_percentage.0);
+                    .position(|step| total_progress as f32 <= step.step.start_percentage.0);
                 prev_keyframe_index = next_keyframe_index
                     .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None })
                     .unwrap_or(0);
             },
             AnimationDirection::Reverse => {
                 next_keyframe_index = self
-                    .keyframes_animation
-                    .steps
+                    .computed_steps
                     .iter()
                     .rev()
-                    .position(|step| total_progress as f32 <= 1. - step.start_percentage.0)
-                    .map(|pos| self.keyframes_animation.steps.len() - pos - 1);
+                    .position(|step| total_progress as f32 <= 1. - step.step.start_percentage.0)
+                    .map(|pos| num_steps - pos - 1);
                 prev_keyframe_index = next_keyframe_index
                     .and_then(|pos| {
-                        if pos != self.keyframes_animation.steps.len() - 1 {
+                        if pos != num_steps - 1 {
                             Some(pos + 1)
                         } else {
                             None
                         }
                     })
-                    .unwrap_or(self.keyframes_animation.steps.len() - 1)
+                    .unwrap_or(num_steps - 1)
             },
             _ => unreachable!(),
         }
 
         debug!(
             "Animation::update_style: keyframe from {:?} to {:?}",
             prev_keyframe_index, next_keyframe_index
         );
 
-        let prev_keyframe = &self.keyframes_animation.steps[prev_keyframe_index];
+        let prev_keyframe = &self.computed_steps[prev_keyframe_index];
         let next_keyframe = match next_keyframe_index {
-            Some(target) => &self.keyframes_animation.steps[target],
+            Some(index) => &self.computed_steps[index],
             None => return,
         };
 
         let update_with_single_keyframe_style = |style, computed_style: &Arc<ComputedValues>| {
             let mutable_style = Arc::make_mut(style);
-            for property in self
-                .keyframes_animation
-                .properties_changed
-                .iter()
-                .filter_map(|longhand| {
-                    AnimationValue::from_computed_values(longhand, &**computed_style)
-                })
-            {
+            for property in self.properties_changed.iter().filter_map(|longhand| {
+                AnimationValue::from_computed_values(longhand, &**computed_style)
+            }) {
                 property.set_in_style_for_servo(mutable_style);
             }
         };
 
         // TODO: How could we optimise it? Is it such a big deal?
-        let prev_keyframe_style = compute_style_for_animation_step::<E>(
-            context,
-            prev_keyframe,
-            style,
-            &self.cascade_style,
-            font_metrics_provider,
-        );
+        let prev_keyframe_style = &prev_keyframe.style;
+        let next_keyframe_style = &next_keyframe.style;
         if total_progress <= 0.0 {
             update_with_single_keyframe_style(style, &prev_keyframe_style);
             return;
         }
 
-        let next_keyframe_style = compute_style_for_animation_step::<E>(
-            context,
-            next_keyframe,
-            &prev_keyframe_style,
-            &self.cascade_style,
-            font_metrics_provider,
-        );
         if total_progress >= 1.0 {
             update_with_single_keyframe_style(style, &next_keyframe_style);
             return;
         }
 
         let relative_timespan =
-            (next_keyframe.start_percentage.0 - prev_keyframe.start_percentage.0).abs();
+            (next_keyframe.step.start_percentage.0 - prev_keyframe.step.start_percentage.0).abs();
         let relative_duration = relative_timespan as f64 * duration;
         let last_keyframe_ended_at = match self.current_direction {
             AnimationDirection::Normal => {
-                self.started_at + (duration * prev_keyframe.start_percentage.0 as f64)
+                self.started_at + (duration * prev_keyframe.step.start_percentage.0 as f64)
             },
             AnimationDirection::Reverse => {
-                self.started_at + (duration * (1. - prev_keyframe.start_percentage.0 as f64))
+                self.started_at + (duration * (1. - prev_keyframe.step.start_percentage.0 as f64))
             },
             _ => unreachable!(),
         };
         let relative_progress = (now - last_keyframe_ended_at) / relative_duration;
 
-        // NB: The spec says that the timing function can be overwritten
-        // from the keyframe style.
-        let timing_function = if prev_keyframe.declared_timing_function {
-            // NB: animation_timing_function can never be empty, always has
-            // at least the default value (`ease`).
-            prev_keyframe_style
-                .get_box()
-                .animation_timing_function_at(0)
-        } else {
-            // TODO(mrobinson): It isn't optimal to have to walk this list every
-            // time. Perhaps this should be stored in the animation.
-            let index = match style
-                .get_box()
-                .animation_name_iter()
-                .position(|animation_name| Some(&self.name) == animation_name.as_atom())
-            {
-                Some(index) => index,
-                None => return warn!("Tried to update a style with a cancelled animation."),
-            };
-            style.get_box().animation_timing_function_mod(index)
-        };
-
         let mut new_style = (**style).clone();
         let mut update_style_for_longhand = |longhand| {
             let from = AnimationValue::from_computed_values(longhand, &prev_keyframe_style)?;
             let to = AnimationValue::from_computed_values(longhand, &next_keyframe_style)?;
             PropertyAnimation {
                 from,
                 to,
-                timing_function,
+                timing_function: prev_keyframe.timing_function,
                 duration: relative_duration as f64,
             }
             .update(&mut new_style, relative_progress);
             None::<()>
         };
 
-        for property in self.keyframes_animation.properties_changed.iter() {
+        for property in self.properties_changed.iter() {
             update_style_for_longhand(property);
         }
 
         *Arc::make_mut(style) = new_style;
     }
 }
 
 impl fmt::Debug for Animation {
@@ -719,26 +773,23 @@ impl ElementAnimationSet {
         for animation in self.animations.iter_mut() {
             animation.state = AnimationState::Canceled;
         }
         for transition in self.transitions.iter_mut() {
             transition.state = AnimationState::Canceled;
         }
     }
 
-    pub(crate) fn apply_active_animations<E>(
+    pub(crate) fn apply_active_animations(
         &mut self,
         context: &SharedStyleContext,
         style: &mut Arc<ComputedValues>,
-        font_metrics: &dyn crate::font_metrics::FontMetricsProvider,
-    ) where
-        E: TElement,
-    {
+    ) {
         for animation in &self.animations {
-            animation.update_style::<E>(context, style, font_metrics);
+            animation.update_style(context, style);
         }
 
         for transition in &self.transitions {
             transition.update_style(context, style);
         }
     }
 
     /// Clear all canceled animations and transitions from this `ElementAnimationSet`.
@@ -773,70 +824,73 @@ impl ElementAnimationSet {
             .filter(|animation| animation.state.needs_to_be_ticked())
             .count() +
             self.transitions
                 .iter()
                 .filter(|transition| transition.state.needs_to_be_ticked())
                 .count()
     }
 
-    fn has_active_transition_or_animation(&self) -> bool {
+    /// If this `ElementAnimationSet` has any any active animations.
+    pub fn has_active_animation(&self) -> bool {
         self.animations
             .iter()
-            .any(|animation| animation.state != AnimationState::Canceled) ||
-            self.transitions
-                .iter()
-                .any(|transition| transition.state != AnimationState::Canceled)
+            .any(|animation| animation.state != AnimationState::Canceled)
+    }
+
+    /// If this `ElementAnimationSet` has any any active transitions.
+    pub fn has_active_transition(&self) -> bool {
+        self.transitions
+            .iter()
+            .any(|transition| transition.state != AnimationState::Canceled)
     }
 
     /// Update our animations given a new style, canceling or starting new animations
     /// when appropriate.
     pub fn update_animations_for_new_style<E>(
         &mut self,
         element: E,
         context: &SharedStyleContext,
         new_style: &Arc<ComputedValues>,
+        font_metrics: &dyn crate::font_metrics::FontMetricsProvider,
     ) where
         E: TElement,
     {
         for animation in self.animations.iter_mut() {
             if animation.is_cancelled_in_new_style(new_style) {
                 animation.state = AnimationState::Canceled;
             }
         }
 
-        maybe_start_animations(element, &context, &new_style, self);
+        maybe_start_animations(element, &context, &new_style, self, font_metrics);
     }
 
     /// Update our transitions given a new style, canceling or starting new animations
     /// when appropriate.
-    pub fn update_transitions_for_new_style<E>(
+    pub fn update_transitions_for_new_style(
         &mut self,
         context: &SharedStyleContext,
         opaque_node: OpaqueNode,
         old_style: Option<&Arc<ComputedValues>>,
         after_change_style: &Arc<ComputedValues>,
-        font_metrics: &dyn crate::font_metrics::FontMetricsProvider,
-    ) where
-        E: TElement,
-    {
+    ) {
         // If this is the first style, we don't trigger any transitions and we assume
         // there were no previously triggered transitions.
         let mut before_change_style = match old_style {
             Some(old_style) => Arc::clone(old_style),
             None => return,
         };
 
         // We convert old values into `before-change-style` here.
         // See https://drafts.csswg.org/css-transitions/#starting. We need to clone the
         // style because this might still be a reference to the original `old_style` and
         // we want to preserve that so that we can later properly calculate restyle damage.
-        if self.has_active_transition_or_animation() {
+        if self.has_active_transition() || self.has_active_animation() {
             before_change_style = before_change_style.clone();
-            self.apply_active_animations::<E>(context, &mut before_change_style, font_metrics);
+            self.apply_active_animations(context, &mut before_change_style);
         }
 
         let transitioning_properties = start_transitions_if_applicable(
             context,
             opaque_node,
             &before_change_style,
             after_change_style,
             self,
@@ -956,100 +1010,52 @@ pub fn start_transitions_if_applicable(
             old_style,
             new_style,
         );
     }
 
     properties_that_transition
 }
 
-fn compute_style_for_animation_step<E>(
-    context: &SharedStyleContext,
-    step: &KeyframesStep,
-    previous_style: &ComputedValues,
-    style_from_cascade: &Arc<ComputedValues>,
-    font_metrics_provider: &dyn FontMetricsProvider,
-) -> Arc<ComputedValues>
-where
-    E: TElement,
-{
-    match step.value {
-        KeyframesStepValue::ComputedValues => style_from_cascade.clone(),
-        KeyframesStepValue::Declarations {
-            block: ref declarations,
-        } => {
-            let guard = declarations.read_with(context.guards.author);
-
-            // This currently ignores visited styles, which seems acceptable,
-            // as existing browsers don't appear to animate visited styles.
-            let computed = properties::apply_declarations::<E, _>(
-                context.stylist.device(),
-                /* pseudo = */ None,
-                previous_style.rules(),
-                &context.guards,
-                // It's possible to have !important properties in keyframes
-                // so we have to filter them out.
-                // See the spec issue https://github.com/w3c/csswg-drafts/issues/1824
-                // Also we filter our non-animatable properties.
-                guard
-                    .normal_declaration_iter()
-                    .filter(|declaration| declaration.is_animatable())
-                    .map(|decl| (decl, Origin::Author)),
-                Some(previous_style),
-                Some(previous_style),
-                Some(previous_style),
-                font_metrics_provider,
-                CascadeMode::Unvisited {
-                    visited_rules: None,
-                },
-                context.quirks_mode(),
-                /* rule_cache = */ None,
-                &mut Default::default(),
-                /* element = */ None,
-            );
-            computed
-        },
-    }
-}
-
 /// Triggers animations for a given node looking at the animation property
 /// values.
 pub fn maybe_start_animations<E>(
     element: E,
     context: &SharedStyleContext,
     new_style: &Arc<ComputedValues>,
     animation_state: &mut ElementAnimationSet,
+    font_metrics_provider: &dyn FontMetricsProvider,
 ) where
     E: TElement,
 {
     let box_style = new_style.get_box();
     for (i, name) in box_style.animation_name_iter().enumerate() {
         let name = match name.as_atom() {
             Some(atom) => atom,
             None => continue,
         };
 
         debug!("maybe_start_animations: name={}", name);
         let duration = box_style.animation_duration_mod(i).seconds();
         if duration == 0. {
             continue;
         }
 
-        let anim = match context.stylist.get_animation(name, element) {
+        let keyframe_animation = match context.stylist.get_animation(name, element) {
             Some(animation) => animation,
             None => continue,
         };
 
         debug!("maybe_start_animations: animation {} found", name);
 
         // If this animation doesn't have any keyframe, we can just continue
         // without submitting it to the compositor, since both the first and
         // the second keyframes would be synthetised from the computed
         // values.
-        if anim.steps.is_empty() {
+        if keyframe_animation.steps.is_empty() {
             continue;
         }
 
         let delay = box_style.animation_delay_mod(i).seconds();
         let animation_start = context.current_time_for_animations + delay as f64;
         let iteration_state = match box_style.animation_iteration_count_mod(i) {
             AnimationIterationCount::Infinite => KeyframesIterationState::Infinite(0.0),
             AnimationIterationCount::Number(n) => KeyframesIterationState::Finite(0.0, n.into()),
@@ -1066,20 +1072,30 @@ pub fn maybe_start_animations<E>(
             },
         };
 
         let state = match box_style.animation_play_state_mod(i) {
             AnimationPlayState::Paused => AnimationState::Paused(0.),
             AnimationPlayState::Running => AnimationState::Pending,
         };
 
+        let computed_steps = ComputedKeyframeStep::generate_for_keyframes::<E>(
+            element,
+            &keyframe_animation.steps,
+            context,
+            new_style,
+            font_metrics_provider,
+            new_style.get_box().animation_timing_function_mod(i),
+        );
+
         let new_animation = Animation {
             node: element.as_node().opaque(),
             name: name.clone(),
-            keyframes_animation: anim.clone(),
+            properties_changed: keyframe_animation.properties_changed,
+            computed_steps,
             started_at: animation_start,
             duration: duration as f64,
             fill_mode: box_style.animation_fill_mode_mod(i),
             delay: delay as f64,
             iteration_state,
             state,
             direction: animation_direction,
             current_direction: initial_direction,
--- a/servo/components/style/dom.rs
+++ b/servo/components/style/dom.rs
@@ -3,20 +3,19 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
 
 //! Types and traits used to access the DOM from style calculation.
 
 #![allow(unsafe_code)]
 #![deny(missing_docs)]
 
 use crate::applicable_declarations::ApplicableDeclarationBlock;
+use crate::context::SharedStyleContext;
 #[cfg(feature = "gecko")]
-use crate::context::PostAnimationTasks;
-#[cfg(feature = "gecko")]
-use crate::context::UpdateAnimationsTasks;
+use crate::context::{PostAnimationTasks, UpdateAnimationsTasks};
 use crate::data::ElementData;
 use crate::element_state::ElementState;
 use crate::font_metrics::FontMetricsProvider;
 use crate::media_queries::Device;
 use crate::properties::{AnimationRules, ComputedValues, PropertyDeclarationBlock};
 use crate::selector_parser::{AttrValue, Lang, PseudoElement, SelectorImpl};
 use crate::shared_lock::Locked;
 use crate::stylist::CascadeData;
@@ -744,17 +743,17 @@ pub trait TElement:
     fn process_post_animation(&self, tasks: PostAnimationTasks);
 
     /// Returns true if the element has relevant animations. Relevant
     /// animations are those animations that are affecting the element's style
     /// or are scheduled to do so in the future.
     fn has_animations(&self) -> bool;
 
     /// Returns true if the element has a CSS animation.
-    fn has_css_animations(&self) -> bool;
+    fn has_css_animations(&self, context: &SharedStyleContext) -> bool;
 
     /// Returns true if the element has a CSS transition (including running transitions and
     /// completed transitions).
     fn has_css_transitions(&self) -> bool;
 
     /// Returns true if the element has animation restyle hints.
     fn has_animation_restyle_hints(&self) -> bool {
         let data = match self.borrow_data() {
--- a/servo/components/style/gecko/wrapper.rs
+++ b/servo/components/style/gecko/wrapper.rs
@@ -1511,17 +1511,17 @@ impl<'le> TElement for GeckoElement<'le>
             );
         }
     }
 
     fn has_animations(&self) -> bool {
         self.may_have_animations() && unsafe { Gecko_ElementHasAnimations(self.0) }
     }
 
-    fn has_css_animations(&self) -> bool {
+    fn has_css_animations(&self, _: &SharedStyleContext) -> bool {
         self.may_have_animations() && unsafe { Gecko_ElementHasCSSAnimations(self.0) }
     }
 
     fn has_css_transitions(&self) -> bool {
         self.may_have_animations() && unsafe { Gecko_ElementHasCSSTransitions(self.0) }
     }
 
     fn might_need_transitions_update(
--- a/servo/components/style/matching.rs
+++ b/servo/components/style/matching.rs
@@ -228,27 +228,26 @@ trait PrivateMatchMethods: TElement {
             RuleInclusion::All,
             PseudoElementResolution::IfApplicable,
         )
         .cascade_style_and_visited_with_default_parents(inputs);
 
         Some(style.0)
     }
 
-    #[cfg(feature = "gecko")]
     fn needs_animations_update(
         &self,
         context: &mut StyleContext<Self>,
         old_style: Option<&ComputedValues>,
         new_style: &ComputedValues,
     ) -> bool {
         let new_box_style = new_style.get_box();
         let new_style_specifies_animations = new_box_style.specifies_animations();
 
-        let has_animations = self.has_css_animations();
+        let has_animations = self.has_css_animations(&context.shared);
         if !new_style_specifies_animations && !has_animations {
             return false;
         }
 
         let old_style = match old_style {
             Some(old) => old,
             // If we have no old style but have animations, we may be a
             // pseudo-element which was re-created without style changes.
@@ -437,47 +436,63 @@ trait PrivateMatchMethods: TElement {
         context: &mut StyleContext<Self>,
         old_values: &mut Option<Arc<ComputedValues>>,
         new_values: &mut Arc<ComputedValues>,
         _restyle_hint: RestyleHint,
         _important_rules_changed: bool,
     ) {
         use crate::animation::AnimationState;
 
+        // We need to call this before accessing the `ElementAnimationSet` from the
+        // map because this call will do a RwLock::read().
+        let needs_animations_update =
+            self.needs_animations_update(context, old_values.as_ref().map(|s| &**s), new_values);
+
         let this_opaque = self.as_node().opaque();
         let shared_context = context.shared;
-        let mut animation_states = shared_context.animation_states.write();
-        let mut animation_state = animation_states.remove(&this_opaque).unwrap_or_default();
+        let mut animation_set = shared_context
+            .animation_states
+            .write()
+            .remove(&this_opaque)
+            .unwrap_or_default();
 
-        animation_state.update_animations_for_new_style(*self, &shared_context, &new_values);
+        // Starting animations is expensive, because we have to recalculate the style
+        // for all the keyframes. We only want to do this if we think that there's a
+        // chance that the animations really changed.
+        if needs_animations_update {
+            animation_set.update_animations_for_new_style::<Self>(
+                *self,
+                &shared_context,
+                &new_values,
+                &context.thread_local.font_metrics_provider,
+            );
+        }
 
-        animation_state.update_transitions_for_new_style::<Self>(
+        animation_set.update_transitions_for_new_style(
             &shared_context,
             this_opaque,
             old_values.as_ref(),
             new_values,
-            &context.thread_local.font_metrics_provider,
         );
 
-        animation_state.apply_active_animations::<Self>(
-            shared_context,
-            new_values,
-            &context.thread_local.font_metrics_provider,
-        );
+        animation_set.apply_active_animations(shared_context, new_values);
 
         // We clear away any finished transitions, but retain animations, because they
         // might still be used for proper calculation of `animation-fill-mode`.
-        animation_state
+        animation_set
             .transitions
             .retain(|transition| transition.state != AnimationState::Finished);
 
         // If the ElementAnimationSet is empty, and don't store it in order to
         // save memory and to avoid extra processing later.
-        if !animation_state.is_empty() {
-            animation_states.insert(this_opaque, animation_state);
+        if !animation_set.is_empty() {
+            shared_context
+                .animation_states
+                .write()
+                .insert(this_opaque, animation_set);
         }
     }
 
     /// Computes and applies non-redundant damage.
     fn accumulate_damage_for(
         &self,
         shared_context: &SharedStyleContext,
         damage: &mut RestyleDamage,
--- a/servo/components/style/properties/properties.mako.rs
+++ b/servo/components/style/properties/properties.mako.rs
@@ -2824,20 +2824,37 @@ pub mod style_structs {
             /// animation-name other than `none`.
             pub fn specifies_animations(&self) -> bool {
                 self.animation_name_iter().any(|name| name.0.is_some())
             }
 
             /// Returns whether there are any transitions specified.
             #[cfg(feature = "servo")]
             pub fn specifies_transitions(&self) -> bool {
+                // TODO(mrobinson): This should check the combined duration and not just
+                // the duration.
                 self.transition_duration_iter()
                     .take(self.transition_property_count())
                     .any(|t| t.seconds() > 0.)
             }
+
+            /// Returns true if animation properties are equal between styles, but without
+            /// considering keyframe data.
+            #[cfg(feature = "servo")]
+            pub fn animations_equals(&self, other: &Self) -> bool {
+                self.animation_name_iter().eq(other.animation_name_iter()) &&
+                self.animation_delay_iter().eq(other.animation_delay_iter()) &&
+                self.animation_direction_iter().eq(other.animation_direction_iter()) &&
+                self.animation_duration_iter().eq(other.animation_duration_iter()) &&
+                self.animation_fill_mode_iter().eq(other.animation_fill_mode_iter()) &&
+                self.animation_iteration_count_iter().eq(other.animation_iteration_count_iter()) &&
+                self.animation_play_state_iter().eq(other.animation_play_state_iter()) &&
+                self.animation_timing_function_iter().eq(other.animation_timing_function_iter())
+            }
+
         % elif style_struct.name == "Column":
             /// Whether this is a multicol style.
             #[cfg(feature = "servo")]
             pub fn is_multicol(&self) -> bool {
                 !self.column_width.is_auto() || !self.column_count.is_auto()
             }
         % endif
     }
@@ -2920,16 +2937,22 @@ pub struct ComputedValues {
 
 impl ComputedValues {
     /// Returns the pseudo-element that this style represents.
     #[cfg(feature = "servo")]
     pub fn pseudo(&self) -> Option<<&PseudoElement> {
         self.pseudo.as_ref()
     }
 
+    /// Returns true if this is the style for a pseudo-element.
+    #[cfg(feature = "servo")]
+    pub fn is_pseudo_style(&self) -> bool {
+        self.pseudo().is_some()
+    }
+
     /// Returns whether this style's display value is equal to contents.
     pub fn is_display_contents(&self) -> bool {
         self.get_box().clone_display().is_contents()
     }
 
     /// Gets a reference to the rule node. Panic if no rule node exists.
     pub fn rules(&self) -> &StrongRuleNode {
         self.rules.as_ref().unwrap()