Bug 1213651 - Avoid server round-trips when displaying animated dom nodes; r=tromey
authorPatrick Brosset <pbrosset@mozilla.com>
Thu, 21 Jan 2016 13:19:58 +0100
changeset 324028 a7b909eabadf197c6b1988a793c329b6213db042
parent 324027 bd50a0b3f94d016c2aae15ea8fcf52bcca37c617
child 324029 cf165d1764d64d1267c0120a01be86c01da0b3d0
push id9826
push usercliu@mozilla.com
push dateThu, 21 Jan 2016 19:40:17 +0000
reviewerstromey
bugs1213651
milestone46.0a1
Bug 1213651 - Avoid server round-trips when displaying animated dom nodes; r=tromey The main change here is that nodeFronts that have already been displayed in the timeline are stored in a WeakMap so they can be retrieved from it next time they're displayed and avoid a server-side round trip which, in turn, causes the UI to flicker. The other change is that now, it is possible to tell the animations actor what is the current walker actor, which allows animation player actors to directly send the NodeActor ID as part of their forms. Which, in most cases, completely eliminates the server round-trip, because the corresponding NodeFronts are already known on the client, so we get them from there. The last change done here is that AnimationTargetNode now becomes a thin wrapper on top of the new DomNodePreview component that was extracted so it can be reused in other places.
.eslintignore
devtools/client/animationinspector/animation-controller.js
devtools/client/animationinspector/components/animation-target-node.js
devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
devtools/client/animationinspector/test/browser_animation_target_highlight_select.js
devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js
devtools/client/animationinspector/test/head.js
devtools/client/animationinspector/utils.js
devtools/client/inspector/shared/dom-node-preview.js
devtools/client/inspector/shared/moz.build
devtools/client/inspector/shared/style-inspector-overlays.js
devtools/client/inspector/shared/utils.js
devtools/client/locales/en-US/animationinspector.properties
devtools/client/locales/en-US/inspector.properties
devtools/server/actors/animation.js
devtools/server/actors/inspector.js
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,28 +1,24 @@
 # Always ignore node_modules.
 **/node_modules/**/*.*
 
 # Exclude expected objdirs.
 obj*/**
 
-# Temporarily ignore HTML files that still need to be fixed.
-devtools/**/*.html
-
 # We ignore all these directories by default, until we get them enabled.
 # If you are enabling a directory, please add directory specific exclusions
 # below.
 accessible/**
 addon-sdk/**
 build/**
 caps/**
 chrome/**
 config/**
 db/**
-devtools/**
 docshell/**
 dom/**
 editor/**
 embedding/**
 extensions/**
 gfx/**
 gradle/**
 hal/**
@@ -86,42 +82,76 @@ browser/extensions/pocket/content/panels
 browser/extensions/shumway/**
 browser/fuel/**
 browser/locales/**
 
 # Ignore all of loop since it is imported from github and checked at source.
 browser/extensions/loop/**
 
 # devtools/ exclusions
-# Ignore d3
-devtools/client/shared/d3.js
-devtools/client/webaudioeditor/lib/dagre-d3.js
+devtools/*.js
+devtools/client/*.js
+devtools/client/aboutdebugging/**
+devtools/client/animationinspector/**
+devtools/client/canvasdebugger/**
+devtools/client/commandline/**
+devtools/client/debugger/**
+devtools/client/eyedropper/**
+devtools/client/framework/**
+# devtools/client/inspector/shared/*.js files are eslint-clean, so they aren't
+# included in the ignore list.
+devtools/client/inspector/computed/**
+devtools/client/inspector/fonts/**
+devtools/client/inspector/layout/**
+devtools/client/inspector/markup/**
+devtools/client/inspector/rules/**
+devtools/client/inspector/shared/test/**
+devtools/client/inspector/test/**
+devtools/client/inspector/*.js
+devtools/client/jsonview/**
+devtools/client/memory/**
+devtools/client/netmonitor/**
+devtools/client/performance/**
+devtools/client/projecteditor/**
+devtools/client/promisedebugger/**
+devtools/client/responsivedesign/**
+devtools/client/scratchpad/**
+devtools/client/shadereditor/**
+devtools/client/shared/**
+devtools/client/sourceeditor/**
+devtools/client/storage/**
+devtools/client/styleeditor/**
+devtools/client/tilt/**
+devtools/client/webaudioeditor/**
+devtools/client/webconsole/**
+devtools/client/webide/**
+devtools/server/**
+devtools/shared/**
 
-# Ignore codemirror
-devtools/client/sourceeditor/codemirror/*.js
-devtools/client/sourceeditor/codemirror/**/*.js
-devtools/client/sourceeditor/test/codemirror/*
-
-# Ignore jquery test libs
-devtools/client/markupview/test/lib_*
-
-# Ignore pre-processed files
+# Ignore devtools pre-processed files
 devtools/client/framework/toolbox-process-window.js
 devtools/client/performance/system.js
 devtools/client/webide/webide-prefs.js
+devtools/client/preferences/**
 
-# Ignore various libs
+# Ignore devtools third-party libs
 devtools/shared/jsbeautify/*
 devtools/shared/acorn/*
 devtools/client/sourceeditor/tern/*
 devtools/shared/pretty-fast/*
 devtools/shared/sourcemap/*
 devtools/shared/qrcode/decoder/*
 devtools/shared/qrcode/encoder/*
 devtools/client/shared/vendor/*
+devtools/client/shared/d3.js
+devtools/client/webaudioeditor/lib/dagre-d3.js
+devtools/client/sourceeditor/codemirror/*.js
+devtools/client/sourceeditor/codemirror/**/*.js
+devtools/client/sourceeditor/test/codemirror/*
+devtools/client/markupview/test/lib_*
 
 # mobile/android/ exclusions
 mobile/android/chrome/content
 mobile/android/tests/
 
 # Uses `#filter substitution`
 mobile/android/b2gdroid/app/b2gdroid.js
 mobile/android/app/mobile.js
--- a/devtools/client/animationinspector/animation-controller.js
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -88,17 +88,19 @@ var getServerTraits = Task.async(functio
       method: "setPlaybackRate" },
     { name: "hasSetPlaybackRates", actor: "animations",
       method: "setPlaybackRates" },
     { name: "hasTargetNode", actor: "domwalker",
       method: "getNodeFromActor" },
     { name: "hasSetCurrentTimes", actor: "animations",
       method: "setCurrentTimes" },
     { name: "hasGetFrames", actor: "animationplayer",
-      method: "getFrames" }
+      method: "getFrames" },
+    { name: "hasSetWalkerActor", actor: "animations",
+      method: "setWalkerActor" },
   ];
 
   let traits = {};
   for (let {name, actor, method} of config) {
     traits[name] = yield target.actorHasMethod(actor, method);
   }
 
   return traits;
@@ -142,16 +144,22 @@ var AnimationsController = {
     // Expose actor capabilities.
     this.traits = yield getServerTraits(target);
 
     if (this.destroyed) {
       console.warn("Could not fully initialize the AnimationsController");
       return;
     }
 
+    // Let the AnimationsActor know what WalkerActor we're using. This will
+    // come in handy later to return references to DOM Nodes.
+    if (this.traits.hasSetWalkerActor) {
+      yield this.animationsFront.setWalkerActor(gInspector.walker);
+    }
+
     this.startListeners();
     yield this.onNewNodeFront();
 
     this.initialized.resolve();
   }),
 
   destroy: Task.async(function*() {
     if (!this.initialized) {
--- a/devtools/client/animationinspector/components/animation-target-node.js
+++ b/devtools/client/animationinspector/components/animation-target-node.js
@@ -1,320 +1,76 @@
 "use strict";
 
 const {Cu} = require("chrome");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
-const {
-  createNode,
-  TargetNodeHighlighter
-} = require("devtools/client/animationinspector/utils");
+const {DomNodePreview} = require(
+  "devtools/client/inspector/shared/dom-node-preview");
 
-const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+// Map dom node fronts by animation fronts so we don't have to get them from the
+// walker every time the timeline is refreshed.
+var nodeFronts = new WeakMap();
 
 /**
  * UI component responsible for displaying a preview of the target dom node of
  * a given animation.
- * @param {InspectorPanel} inspector Requires a reference to the inspector-panel
- * to highlight and select the node, as well as refresh it when there are
- * mutations.
- * @param {Object} options Supported properties are:
- * - compact {Boolean} Defaults to false. If true, nodes will be previewed like
- *   tag#id.class instead of <tag id="id" class="class">
+ * Accepts the same parameters as the DomNodePreview component. See
+ * devtools/client/inspector/shared/dom-node-preview.js for documentation.
  */
-function AnimationTargetNode(inspector, options = {}) {
+function AnimationTargetNode(inspector, options) {
   this.inspector = inspector;
-  this.options = options;
-
-  this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
-  this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
-  this.onSelectNodeClick = this.onSelectNodeClick.bind(this);
-  this.onMarkupMutations = this.onMarkupMutations.bind(this);
-  this.onHighlightNodeClick = this.onHighlightNodeClick.bind(this);
-  this.onTargetHighlighterLocked = this.onTargetHighlighterLocked.bind(this);
-
+  this.previewer = new DomNodePreview(inspector, options);
   EventEmitter.decorate(this);
 }
 
 exports.AnimationTargetNode = AnimationTargetNode;
 
 AnimationTargetNode.prototype = {
   init: function(containerEl) {
-    let document = containerEl.ownerDocument;
-
-    // Init the markup for displaying the target node.
-    this.el = createNode({
-      parent: containerEl,
-      attributes: {
-        "class": "animation-target"
-      }
-    });
-
-    // Icon to select the node in the inspector.
-    this.highlightNodeEl = createNode({
-      parent: this.el,
-      nodeType: "span",
-      attributes: {
-        "class": "node-highlighter",
-        "title": L10N.getStr("node.highlightNodeLabel")
-      }
-    });
-
-    // Wrapper used for mouseover/out event handling.
-    this.previewEl = createNode({
-      parent: this.el,
-      nodeType: "span",
-      attributes: {
-        "title": L10N.getStr("node.selectNodeLabel")
-      }
-    });
-
-    if (!this.options.compact) {
-      this.previewEl.appendChild(document.createTextNode("<"));
-    }
-
-    // Tag name.
-    this.tagNameEl = createNode({
-      parent: this.previewEl,
-      nodeType: "span",
-      attributes: {
-        "class": "tag-name theme-fg-color3"
-      }
-    });
-
-    // Id attribute container.
-    this.idEl = createNode({
-      parent: this.previewEl,
-      nodeType: "span"
-    });
-
-    if (!this.options.compact) {
-      createNode({
-        parent: this.idEl,
-        nodeType: "span",
-        attributes: {
-          "class": "attribute-name theme-fg-color2"
-        },
-        textContent: "id"
-      });
-      this.idEl.appendChild(document.createTextNode("=\""));
-    } else {
-      createNode({
-        parent: this.idEl,
-        nodeType: "span",
-        attributes: {
-          "class": "theme-fg-color2"
-        },
-        textContent: "#"
-      });
-    }
-
-    createNode({
-      parent: this.idEl,
-      nodeType: "span",
-      attributes: {
-        "class": "attribute-value theme-fg-color6"
-      }
-    });
-
-    if (!this.options.compact) {
-      this.idEl.appendChild(document.createTextNode("\""));
-    }
-
-    // Class attribute container.
-    this.classEl = createNode({
-      parent: this.previewEl,
-      nodeType: "span"
-    });
-
-    if (!this.options.compact) {
-      createNode({
-        parent: this.classEl,
-        nodeType: "span",
-        attributes: {
-          "class": "attribute-name theme-fg-color2"
-        },
-        textContent: "class"
-      });
-      this.classEl.appendChild(document.createTextNode("=\""));
-    } else {
-      createNode({
-        parent: this.classEl,
-        nodeType: "span",
-        attributes: {
-          "class": "theme-fg-color6"
-        },
-        textContent: "."
-      });
-    }
-
-    createNode({
-      parent: this.classEl,
-      nodeType: "span",
-      attributes: {
-        "class": "attribute-value theme-fg-color6"
-      }
-    });
-
-    if (!this.options.compact) {
-      this.classEl.appendChild(document.createTextNode("\""));
-      this.previewEl.appendChild(document.createTextNode(">"));
-    }
-
-    this.startListeners();
-  },
-
-  startListeners: function() {
-    // Init events for highlighting and selecting the node.
-    this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
-    this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
-    this.previewEl.addEventListener("click", this.onSelectNodeClick);
-    this.highlightNodeEl.addEventListener("click", this.onHighlightNodeClick);
-
-    // Start to listen for markupmutation events.
-    this.inspector.on("markupmutation", this.onMarkupMutations);
-
-    // Listen to the target node highlighter.
-    TargetNodeHighlighter.on("highlighted", this.onTargetHighlighterLocked);
-  },
-
-  stopListeners: function() {
-    TargetNodeHighlighter.off("highlighted", this.onTargetHighlighterLocked);
-    this.inspector.off("markupmutation", this.onMarkupMutations);
-    this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
-    this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
-    this.previewEl.removeEventListener("click", this.onSelectNodeClick);
-    this.highlightNodeEl.removeEventListener("click", this.onHighlightNodeClick);
+    this.previewer.init(containerEl);
+    this.isDestroyed = false;
   },
 
   destroy: function() {
-    TargetNodeHighlighter.unhighlight().catch(e => console.error(e));
-
-    this.stopListeners();
-
-    this.el.remove();
-    this.el = this.tagNameEl = this.idEl = this.classEl = null;
-    this.highlightNodeEl = this.previewEl = null;
-    this.nodeFront = this.inspector = this.playerFront = null;
-  },
-
-  get highlighterUtils() {
-    if (this.inspector && this.inspector.toolbox) {
-      return this.inspector.toolbox.highlighterUtils;
-    }
-    return null;
-  },
-
-  onPreviewMouseOver: function() {
-    if (!this.nodeFront || !this.highlighterUtils) {
-      return;
-    }
-    this.highlighterUtils.highlightNodeFront(this.nodeFront)
-                         .catch(e => console.error(e));
-  },
-
-  onPreviewMouseOut: function() {
-    if (!this.nodeFront || !this.highlighterUtils) {
-      return;
-    }
-    this.highlighterUtils.unhighlight()
-                         .catch(e => console.error(e));
-  },
-
-  onSelectNodeClick: function() {
-    if (!this.nodeFront) {
-      return;
-    }
-    this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
-  },
-
-  onHighlightNodeClick: function(e) {
-    e.stopPropagation();
-
-    let classList = this.highlightNodeEl.classList;
-
-    let isHighlighted = classList.contains("selected");
-    if (isHighlighted) {
-      classList.remove("selected");
-      TargetNodeHighlighter.unhighlight().then(() => {
-        this.emit("target-highlighter-unlocked");
-      }, e => console.error(e));
-    } else {
-      classList.add("selected");
-      TargetNodeHighlighter.highlight(this).then(() => {
-        this.emit("target-highlighter-locked");
-      }, e => console.error(e));
-    }
-  },
-
-  onTargetHighlighterLocked: function(e, animationTargetNode) {
-    if (animationTargetNode !== this) {
-      this.highlightNodeEl.classList.remove("selected");
-    }
-  },
-
-  onMarkupMutations: function(e, mutations) {
-    if (!this.nodeFront || !this.playerFront) {
-      return;
-    }
-
-    for (let {target} of mutations) {
-      if (target === this.nodeFront) {
-        // Re-render with the same nodeFront to update the output.
-        this.render(this.playerFront);
-        break;
-      }
-    }
+    this.previewer.destroy();
+    this.inspector = null;
+    this.isDestroyed = true;
   },
 
   render: Task.async(function*(playerFront) {
-    this.playerFront = playerFront;
-    this.nodeFront = undefined;
+    // Get the nodeFront from the cache if it was stored previously.
+    let nodeFront = nodeFronts.get(playerFront);
 
-    try {
-      this.nodeFront = yield this.inspector.walker.getNodeFromActor(
-                             playerFront.actorID, ["node"]);
-    } catch (e) {
-      if (!this.el) {
-        // The panel was destroyed in the meantime. Just log a warning.
-        console.warn("Cound't retrieve the animation target node, widget " +
-                     "destroyed");
-      } else {
-        // This was an unexpected error, log it.
-        console.error(e);
-      }
-      return;
-    }
-
-    if (!this.nodeFront || !this.el) {
-      return;
+    // Try and get it from the playerFront directly next.
+    if (!nodeFront) {
+      nodeFront = playerFront.animationTargetNodeFront;
     }
 
-    let {tagName, attributes} = this.nodeFront;
-
-    this.tagNameEl.textContent = tagName.toLowerCase();
+    // Finally, get it from the walkerActor if it wasn't found.
+    if (!nodeFront) {
+      try {
+        nodeFront = yield this.inspector.walker.getNodeFromActor(
+                               playerFront.actorID, ["node"]);
+      } catch (e) {
+        // If an error occured while getting the nodeFront and if it can't be
+        // attributed to the panel having been destroyed in the meantime, this
+        // error needs to be logged and render needs to stop.
+        if (!this.isDestroyed) {
+          console.error(e);
+        }
+        return;
+      }
 
-    let idIndex = attributes.findIndex(({name}) => name === "id");
-    if (idIndex > -1 && attributes[idIndex].value) {
-      this.idEl.querySelector(".attribute-value").textContent =
-        attributes[idIndex].value;
-      this.idEl.style.display = "inline";
-    } else {
-      this.idEl.style.display = "none";
+      // In all cases, if by now the panel doesn't exist anymore, we need to
+      // stop rendering too.
+      if (this.isDestroyed) {
+        return;
+      }
     }
 
-    let classIndex = attributes.findIndex(({name}) => name === "class");
-    if (classIndex > -1 && attributes[classIndex].value) {
-      let value = attributes[classIndex].value;
-      if (this.options.compact) {
-        value = value.split(" ").join(".");
-      }
+    // Add the nodeFront to the cache.
+    nodeFronts.set(playerFront, nodeFront);
 
-      this.classEl.querySelector(".attribute-value").textContent = value;
-      this.classEl.style.display = "inline";
-    } else {
-      this.classEl.style.display = "none";
-    }
-
+    this.previewer.render(nodeFront);
     this.emit("target-retrieved");
   })
 };
--- a/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
+++ b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
@@ -20,12 +20,12 @@ add_task(function*() {
   }
   yield waitForAllAnimationTargets(panel);
 
   is(panel.animationsTimelineComponent.animations.length, 3,
      "The timeline shows 3 animations too");
 
   // Reduce the known nodeFronts to a set to make them unique.
   let nodeFronts = new Set(panel.animationsTimelineComponent
-                                .targetNodes.map(n => n.nodeFront));
+                                .targetNodes.map(n => n.previewer.nodeFront));
   is(nodeFronts.size, 3,
      "The animations are applied to 3 different node fronts");
 });
--- a/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
+++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
@@ -11,21 +11,23 @@ requestLongerTimeout(2);
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   info("Select the simple animated node");
   yield selectNode(".animated", inspector);
 
   let targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
+  let {previewer} = targetNodeComponent;
+
   // Make sure to wait for the target-retrieved event if the nodeFront hasn't
   // yet been retrieved by the TargetNodeComponent.
-  if (!targetNodeComponent.nodeFront) {
+  if (!previewer.nodeFront) {
     yield targetNodeComponent.once("target-retrieved");
   }
 
-  is(targetNodeComponent.el.textContent, "div#.ball.animated",
+  is(previewer.el.textContent, "div#.ball.animated",
     "The target element's content is correct");
 
-  let highlighterEl = targetNodeComponent.el.querySelector(".node-highlighter");
+  let highlighterEl = previewer.el.querySelector(".node-highlighter");
   ok(highlighterEl,
     "The icon to highlight the target element in the page exists");
 });
--- a/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js
+++ b/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js
@@ -19,32 +19,32 @@ add_task(function*() {
   yield selectNode(".animated", inspector);
   yield onPanelUpdated;
 
   let targets = yield waitForAllAnimationTargets(panel);
   // Arbitrary select the first one
   let targetNodeComponent = targets[0];
 
   info("Retrieve the part of the widget that highlights the node on hover");
-  let highlightingEl = targetNodeComponent.previewEl;
+  let highlightingEl = targetNodeComponent.previewer.previewEl;
 
   info("Listen to node-highlight event and mouse over the widget");
   let onHighlight = toolbox.once("node-highlight");
   EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
                              highlightingEl.ownerDocument.defaultView);
   let nodeFront = yield onHighlight;
 
   // Do not forget to mouseout, otherwise we get random mouseover event
   // when selecting another node, which triggers some requests in animation
   // inspector.
   EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseout"},
                              highlightingEl.ownerDocument.defaultView);
 
   ok(true, "The node-highlight event was fired");
-  is(targetNodeComponent.nodeFront, nodeFront,
+  is(targetNodeComponent.previewer.nodeFront, nodeFront,
     "The highlighted node is the one stored on the animation widget");
   is(nodeFront.tagName, "DIV",
     "The highlighted node has the correct tagName");
   is(nodeFront.attributes[0].name, "class",
     "The highlighted node has the correct attributes");
   is(nodeFront.attributes[0].value, "ball animated",
     "The highlighted node has the correct class");
 
@@ -55,19 +55,19 @@ add_task(function*() {
 
   targets = yield waitForAllAnimationTargets(panel);
   targetNodeComponent = targets[0];
 
   info("Click on the first animated node component and wait for the " +
        "selection to change");
   let onSelection = inspector.selection.once("new-node-front");
   onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
-  let nodeEl = targetNodeComponent.previewEl;
+  let nodeEl = targetNodeComponent.previewer.previewEl;
   EventUtils.sendMouseEvent({type: "click"}, nodeEl,
                             nodeEl.ownerDocument.defaultView);
   yield onSelection;
 
-  is(inspector.selection.nodeFront, targetNodeComponent.nodeFront,
+  is(inspector.selection.nodeFront, targetNodeComponent.previewer.nodeFront,
     "The selected node is the one stored on the animation widget");
 
   yield onPanelUpdated;
   yield waitForAllAnimationTargets(panel);
 });
--- a/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js
+++ b/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js
@@ -6,47 +6,49 @@
 
 requestLongerTimeout(2);
 
 // Test that the DOM element targets displayed in animation player widgets can
 // be used to highlight elements in the DOM and select them in the inspector.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
-  let {toolbox, inspector, panel} = yield openAnimationInspector();
+  let {panel} = yield openAnimationInspector();
 
   let targets = panel.animationsTimelineComponent.targetNodes;
 
   info("Click on the highlighter icon for the first animated node");
-  yield lockHighlighterOn(targets[0]);
-  ok(targets[0].highlightNodeEl.classList.contains("selected"),
+  let domNodePreview1 = targets[0].previewer;
+  yield lockHighlighterOn(domNodePreview1);
+  ok(domNodePreview1.highlightNodeEl.classList.contains("selected"),
      "The highlighter icon is selected");
 
   info("Click on the highlighter icon for the second animated node");
-  yield lockHighlighterOn(targets[1]);
-  ok(targets[1].highlightNodeEl.classList.contains("selected"),
+  let domNodePreview2 = targets[1].previewer;
+  yield lockHighlighterOn(domNodePreview2);
+  ok(domNodePreview2.highlightNodeEl.classList.contains("selected"),
      "The highlighter icon is selected");
-  ok(!targets[0].highlightNodeEl.classList.contains("selected"),
+  ok(!domNodePreview1.highlightNodeEl.classList.contains("selected"),
      "The highlighter icon for the first node is unselected");
 
   info("Click again to unhighlight");
-  yield unlockHighlighterOn(targets[1]);
-  ok(!targets[1].highlightNodeEl.classList.contains("selected"),
+  yield unlockHighlighterOn(domNodePreview2);
+  ok(!domNodePreview2.highlightNodeEl.classList.contains("selected"),
      "The highlighter icon for the second node is unselected");
 });
 
-function* lockHighlighterOn(targetComponent) {
-  let onLocked = targetComponent.once("target-highlighter-locked");
-  clickOnHighlighterIcon(targetComponent);
+function* lockHighlighterOn(domNodePreview) {
+  let onLocked = domNodePreview.once("target-highlighter-locked");
+  clickOnHighlighterIcon(domNodePreview);
   yield onLocked;
 }
 
-function* unlockHighlighterOn(targetComponent) {
-  let onUnlocked = targetComponent.once("target-highlighter-unlocked");
-  clickOnHighlighterIcon(targetComponent);
+function* unlockHighlighterOn(domNodePreview) {
+  let onUnlocked = domNodePreview.once("target-highlighter-unlocked");
+  clickOnHighlighterIcon(domNodePreview);
   yield onUnlocked;
 }
 
-function clickOnHighlighterIcon(targetComponent) {
-  let lockEl = targetComponent.highlightNodeEl;
+function clickOnHighlighterIcon(domNodePreview) {
+  let lockEl = domNodePreview.highlightNodeEl;
   EventUtils.sendMouseEvent({type: "click"}, lockEl,
                             lockEl.ownerDocument.defaultView);
 }
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -346,17 +346,17 @@ function isNodeVisible(node) {
  * Wait for all AnimationTargetNode instances to be fully loaded
  * (fetched their related actor and rendered), and return them.
  * @param {AnimationsPanel} panel
  * @return {Array} all AnimationTargetNode instances
  */
 var waitForAllAnimationTargets = Task.async(function*(panel) {
   let targets = panel.animationsTimelineComponent.targetNodes;
   yield promise.all(targets.map(t => {
-    if (!t.nodeFront) {
+    if (!t.previewer.nodeFront) {
       return t.once("target-retrieved");
     }
     return false;
   }));
   return targets;
 });
 
 /**
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -142,53 +142,16 @@ function findOptimalTimeInterval(timeSca
     }
     return scaledStep;
   }
 }
 
 exports.findOptimalTimeInterval = findOptimalTimeInterval;
 
 /**
- * The TargetNodeHighlighter util is a helper for AnimationTargetNode components
- * that is used to lock the highlighter on animated nodes in the page.
- * It instantiates a new highlighter that is then shared amongst all instances
- * of AnimationTargetNode. This is useful because that means showing the
- * highlighter on one animated node will unhighlight the previously highlighted
- * one, but will not interfere with the default inspector highlighter.
- */
-var TargetNodeHighlighter = {
-  highlighter: null,
-  isShown: false,
-
-  highlight: Task.async(function*(animationTargetNode) {
-    if (!this.highlighter) {
-      let hUtils = animationTargetNode.inspector.toolbox.highlighterUtils;
-      this.highlighter = yield hUtils.getHighlighterByType("BoxModelHighlighter");
-    }
-
-    yield this.highlighter.show(animationTargetNode.nodeFront);
-    this.isShown = true;
-    this.emit("highlighted", animationTargetNode);
-  }),
-
-  unhighlight: Task.async(function*() {
-    if (!this.highlighter || !this.isShown) {
-      return;
-    }
-
-    yield this.highlighter.hide();
-    this.isShown = false;
-    this.emit("unhighlighted");
-  })
-};
-
-EventEmitter.decorate(TargetNodeHighlighter);
-exports.TargetNodeHighlighter = TargetNodeHighlighter;
-
-/**
  * Format a timestamp (in ms) as a mm:ss.mmm string.
  * @param {Number} time
  * @return {String}
  */
 function formatStopwatchTime(time) {
   // Format falsy values as 0
   if (!time) {
     return "00:00.000";
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/shared/dom-node-preview.js
@@ -0,0 +1,333 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {Cu} = require("chrome");
+Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
+const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+const {createNode} = require("devtools/client/animationinspector/utils");
+
+const STRINGS_URI = "chrome://devtools/locale/inspector.properties";
+const L10N = new ViewHelpers.L10N(STRINGS_URI);
+
+/**
+ * UI component responsible for displaying a preview of a dom node.
+ * @param {InspectorPanel} inspector Requires a reference to the inspector-panel
+ * to highlight and select the node, as well as refresh it when there are
+ * mutations.
+ * @param {Object} options Supported properties are:
+ * - compact {Boolean} Defaults to false.
+ *   By default, nodes are previewed like <tag id="id" class="class">
+ *   If true, nodes will be previewed like tag#id.class instead.
+ */
+function DomNodePreview(inspector, options = {}) {
+  this.inspector = inspector;
+  this.options = options;
+
+  this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
+  this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
+  this.onSelectElClick = this.onSelectElClick.bind(this);
+  this.onMarkupMutations = this.onMarkupMutations.bind(this);
+  this.onHighlightElClick = this.onHighlightElClick.bind(this);
+  this.onHighlighterLocked = this.onHighlighterLocked.bind(this);
+
+  EventEmitter.decorate(this);
+}
+
+exports.DomNodePreview = DomNodePreview;
+
+DomNodePreview.prototype = {
+  init: function(containerEl) {
+    let document = containerEl.ownerDocument;
+
+    // Init the markup for displaying the target node.
+    this.el = createNode({
+      parent: containerEl,
+      attributes: {
+        "class": "animation-target"
+      }
+    });
+
+    // Icon to select the node in the inspector.
+    this.highlightNodeEl = createNode({
+      parent: this.el,
+      nodeType: "span",
+      attributes: {
+        "class": "node-highlighter",
+        "title": L10N.getStr("inspector.nodePreview.highlightNodeLabel")
+      }
+    });
+
+    // Wrapper used for mouseover/out event handling.
+    this.previewEl = createNode({
+      parent: this.el,
+      nodeType: "span",
+      attributes: {
+        "title": L10N.getStr("inspector.nodePreview.selectNodeLabel")
+      }
+    });
+
+    if (!this.options.compact) {
+      this.previewEl.appendChild(document.createTextNode("<"));
+    }
+
+    // Tag name.
+    this.tagNameEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span",
+      attributes: {
+        "class": "tag-name theme-fg-color3"
+      }
+    });
+
+    // Id attribute container.
+    this.idEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span"
+    });
+
+    if (!this.options.compact) {
+      createNode({
+        parent: this.idEl,
+        nodeType: "span",
+        attributes: {
+          "class": "attribute-name theme-fg-color2"
+        },
+        textContent: "id"
+      });
+      this.idEl.appendChild(document.createTextNode("=\""));
+    } else {
+      createNode({
+        parent: this.idEl,
+        nodeType: "span",
+        attributes: {
+          "class": "theme-fg-color6"
+        },
+        textContent: "#"
+      });
+    }
+
+    createNode({
+      parent: this.idEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-value theme-fg-color6"
+      }
+    });
+
+    if (!this.options.compact) {
+      this.idEl.appendChild(document.createTextNode("\""));
+    }
+
+    // Class attribute container.
+    this.classEl = createNode({
+      parent: this.previewEl,
+      nodeType: "span"
+    });
+
+    if (!this.options.compact) {
+      createNode({
+        parent: this.classEl,
+        nodeType: "span",
+        attributes: {
+          "class": "attribute-name theme-fg-color2"
+        },
+        textContent: "class"
+      });
+      this.classEl.appendChild(document.createTextNode("=\""));
+    } else {
+      createNode({
+        parent: this.classEl,
+        nodeType: "span",
+        attributes: {
+          "class": "theme-fg-color6"
+        },
+        textContent: "."
+      });
+    }
+
+    createNode({
+      parent: this.classEl,
+      nodeType: "span",
+      attributes: {
+        "class": "attribute-value theme-fg-color6"
+      }
+    });
+
+    if (!this.options.compact) {
+      this.classEl.appendChild(document.createTextNode("\""));
+      this.previewEl.appendChild(document.createTextNode(">"));
+    }
+
+    this.startListeners();
+  },
+
+  startListeners: function() {
+    // Init events for highlighting and selecting the node.
+    this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
+    this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
+    this.previewEl.addEventListener("click", this.onSelectElClick);
+    this.highlightNodeEl.addEventListener("click", this.onHighlightElClick);
+
+    // Start to listen for markupmutation events.
+    this.inspector.on("markupmutation", this.onMarkupMutations);
+
+    // Listen to the target node highlighter.
+    HighlighterLock.on("highlighted", this.onHighlighterLocked);
+  },
+
+  stopListeners: function() {
+    HighlighterLock.off("highlighted", this.onHighlighterLocked);
+    this.inspector.off("markupmutation", this.onMarkupMutations);
+    this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
+    this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
+    this.previewEl.removeEventListener("click", this.onSelectElClick);
+    this.highlightNodeEl.removeEventListener("click", this.onHighlightElClick);
+  },
+
+  destroy: function() {
+    HighlighterLock.unhighlight().catch(e => console.error(e));
+
+    this.stopListeners();
+
+    this.el.remove();
+    this.el = this.tagNameEl = this.idEl = this.classEl = null;
+    this.highlightNodeEl = this.previewEl = null;
+    this.nodeFront = this.inspector = null;
+  },
+
+  get highlighterUtils() {
+    if (this.inspector && this.inspector.toolbox) {
+      return this.inspector.toolbox.highlighterUtils;
+    }
+    return null;
+  },
+
+  onPreviewMouseOver: function() {
+    if (!this.nodeFront || !this.highlighterUtils) {
+      return;
+    }
+    this.highlighterUtils.highlightNodeFront(this.nodeFront)
+                         .catch(e => console.error(e));
+  },
+
+  onPreviewMouseOut: function() {
+    if (!this.nodeFront || !this.highlighterUtils) {
+      return;
+    }
+    this.highlighterUtils.unhighlight()
+                         .catch(e => console.error(e));
+  },
+
+  onSelectElClick: function() {
+    if (!this.nodeFront) {
+      return;
+    }
+    this.inspector.selection.setNodeFront(this.nodeFront, "dom-node-preview");
+  },
+
+  onHighlightElClick: function(e) {
+    e.stopPropagation();
+
+    let classList = this.highlightNodeEl.classList;
+    let isHighlighted = classList.contains("selected");
+
+    if (isHighlighted) {
+      classList.remove("selected");
+      HighlighterLock.unhighlight().then(() => {
+        this.emit("target-highlighter-unlocked");
+      }, error => console.error(error));
+    } else {
+      classList.add("selected");
+      HighlighterLock.highlight(this).then(() => {
+        this.emit("target-highlighter-locked");
+      }, error => console.error(error));
+    }
+  },
+
+  onHighlighterLocked: function(e, domNodePreview) {
+    if (domNodePreview !== this) {
+      this.highlightNodeEl.classList.remove("selected");
+    }
+  },
+
+  onMarkupMutations: function(e, mutations) {
+    if (!this.nodeFront) {
+      return;
+    }
+
+    for (let {target} of mutations) {
+      if (target === this.nodeFront) {
+        // Re-render with the same nodeFront to update the output.
+        this.render(this.nodeFront);
+        break;
+      }
+    }
+  },
+
+  render: function(nodeFront) {
+    this.nodeFront = nodeFront;
+    let {tagName, attributes} = nodeFront;
+
+    this.tagNameEl.textContent = tagName.toLowerCase();
+
+    let idIndex = attributes.findIndex(({name}) => name === "id");
+    if (idIndex > -1 && attributes[idIndex].value) {
+      this.idEl.querySelector(".attribute-value").textContent =
+        attributes[idIndex].value;
+      this.idEl.style.display = "inline";
+    } else {
+      this.idEl.style.display = "none";
+    }
+
+    let classIndex = attributes.findIndex(({name}) => name === "class");
+    if (classIndex > -1 && attributes[classIndex].value) {
+      let value = attributes[classIndex].value;
+      if (this.options.compact) {
+        value = value.split(" ").join(".");
+      }
+
+      this.classEl.querySelector(".attribute-value").textContent = value;
+      this.classEl.style.display = "inline";
+    } else {
+      this.classEl.style.display = "none";
+    }
+  }
+};
+
+/**
+ * HighlighterLock is a helper used to lock the highlighter on DOM nodes in the
+ * page.
+ * It instantiates a new highlighter that is then shared amongst all instances
+ * of DomNodePreview. This is useful because that means showing the highlighter
+ * on one node will unhighlight the previously highlighted one, but will not
+ * interfere with the default inspector highlighter.
+ */
+var HighlighterLock = {
+  highlighter: null,
+  isShown: false,
+
+  highlight: Task.async(function*(animationTargetNode) {
+    if (!this.highlighter) {
+      let util = animationTargetNode.inspector.toolbox.highlighterUtils;
+      this.highlighter = yield util.getHighlighterByType("BoxModelHighlighter");
+    }
+
+    yield this.highlighter.show(animationTargetNode.nodeFront);
+    this.isShown = true;
+    this.emit("highlighted", animationTargetNode);
+  }),
+
+  unhighlight: Task.async(function*() {
+    if (!this.highlighter || !this.isShown) {
+      return;
+    }
+
+    yield this.highlighter.hide();
+    this.isShown = false;
+    this.emit("unhighlighted");
+  })
+};
+
+EventEmitter.decorate(HighlighterLock);
--- a/devtools/client/inspector/shared/moz.build
+++ b/devtools/client/inspector/shared/moz.build
@@ -1,13 +1,14 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'dom-node-preview.js',
     'style-inspector-menu.js',
     'style-inspector-overlays.js',
     'utils.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/shared/style-inspector-overlays.js
+++ b/devtools/client/inspector/shared/style-inspector-overlays.js
@@ -27,21 +27,26 @@ Cu.import("resource://gre/modules/Servic
 
 const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
 
 // Types of existing tooltips
 const TOOLTIP_IMAGE_TYPE = "image";
 const TOOLTIP_FONTFAMILY_TYPE = "font-family";
 
 // Types of nodes in the rule/computed-view
-const VIEW_NODE_SELECTOR_TYPE = exports.VIEW_NODE_SELECTOR_TYPE = 1;
-const VIEW_NODE_PROPERTY_TYPE = exports.VIEW_NODE_PROPERTY_TYPE = 2;
-const VIEW_NODE_VALUE_TYPE = exports.VIEW_NODE_VALUE_TYPE = 3;
-const VIEW_NODE_IMAGE_URL_TYPE = exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
-const VIEW_NODE_LOCATION_TYPE = exports.VIEW_NODE_LOCATION_TYPE = 5;
+const VIEW_NODE_SELECTOR_TYPE = 1;
+exports.VIEW_NODE_SELECTOR_TYPE = VIEW_NODE_SELECTOR_TYPE;
+const VIEW_NODE_PROPERTY_TYPE = 2;
+exports.VIEW_NODE_PROPERTY_TYPE = VIEW_NODE_PROPERTY_TYPE;
+const VIEW_NODE_VALUE_TYPE = 3;
+exports.VIEW_NODE_VALUE_TYPE = VIEW_NODE_VALUE_TYPE;
+const VIEW_NODE_IMAGE_URL_TYPE = 4;
+exports.VIEW_NODE_IMAGE_URL_TYPE = VIEW_NODE_IMAGE_URL_TYPE;
+const VIEW_NODE_LOCATION_TYPE = 5;
+exports.VIEW_NODE_LOCATION_TYPE = VIEW_NODE_LOCATION_TYPE;
 
 /**
  * Manages all highlighters in the style-inspector.
  *
  * @param {CssRuleView|CssComputedView} view
  *        Either the rule-view or computed-view panel
  */
 function HighlightersOverlay(view) {
@@ -178,19 +183,19 @@ HighlightersOverlay.prototype = {
     if (!this.highlighterShown || !this.highlighters[this.highlighterShown]) {
       return;
     }
 
     // For some reason, the call to highlighter.hide doesn't always return a
     // promise. This causes some tests to fail when trying to install a
     // rejection handler on the result of the call. To avoid this, check
     // whether the result is truthy before installing the handler.
-    let promise = this.highlighters[this.highlighterShown].hide();
-    if (promise) {
-      promise.then(null, e => console.error(e));
+    let onHidden = this.highlighters[this.highlighterShown].hide();
+    if (onHidden) {
+      onHidden.then(null, e => console.error(e));
     }
 
     this.highlighterShown = null;
     this.emit("highlighter-hidden");
   },
 
   /**
    * Get a highlighter front given a type. It will only be initialized once
--- a/devtools/client/inspector/shared/utils.js
+++ b/devtools/client/inspector/shared/utils.js
@@ -1,17 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {Cc, Ci, Cu} = require("chrome");
+const {Ci, Cu} = require("chrome");
 const {setTimeout, clearTimeout} =
       Cu.import("resource://gre/modules/Timer.jsm", {});
 const {parseDeclarations} =
       require("devtools/client/shared/css-parsing-utils");
 const promise = require("promise");
 
 loader.lazyServiceGetter(this, "domUtils",
   "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
@@ -23,17 +23,17 @@ const HTML_NS = "http://www.w3.org/1999/
  *
  * @param {Element} parent
  *        The parent node.
  * @param {string} tagName
  *        The tag name.
  * @param {object} attributes
  *        A set of attributes to set on the node.
  */
-function createChild(parent, tagName, attributes={}) {
+function createChild(parent, tagName, attributes = {}) {
   let elt = parent.ownerDocument.createElementNS(HTML_NS, tagName);
   for (let attr in attributes) {
     if (attributes.hasOwnProperty(attr)) {
       if (attr === "textContent") {
         elt.textContent = attributes[attr];
       } else if (attr === "child") {
         elt.appendChild(attributes[attr]);
       } else {
--- a/devtools/client/locales/en-US/animationinspector.properties
+++ b/devtools/client/locales/en-US/animationinspector.properties
@@ -88,22 +88,8 @@ timeline.cssanimation.nameLabel=%S - CSS
 timeline.csstransition.nameLabel=%S - CSS Transition
 
 # LOCALIZATION NOTE (timeline.unknown.nameLabel):
 # This string is displayed in a tooltip of the animation panel that is shown
 # when hovering over the name of an unknown animation type in the timeline UI.
 # This can happen if devtools couldn't figure out the type of the animation.
 # %S will be replaced by the name of the transition at run-time.
 timeline.unknown.nameLabel=%S
-
-# LOCALIZATION NOTE (node.selectNodeLabel):
-# This string is displayed in a tooltip of the animation panel that is shown
-# when hovering over an animated node (e.g. something like div.animated).
-# The tooltip invites the user to click on the node in order to select it in the
-# inspector panel.
-node.selectNodeLabel=Click to select this node in the Inspector
-
-# LOCALIZATION NOTE (node.highlightNodeLabel):
-# This string is displayed in a tooltip of the animation panel that is shown
-# when hovering over the inspector icon displayed next to animated nodes.
-# The tooltip invites the user to click on the icon in order to show the node
-# highlighter.
-node.highlightNodeLabel=Click to highlight this node in the page
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -111,8 +111,27 @@ inspector.menu.selectElement.label=Selec
 # to edit an attribute on this node.
 inspector.menu.editAttribute.label=Edit Attribute %S
 
 # LOCALIZATION NOTE (inspector.menu.removeAttribute.label): This is the label of a
 # sub-menu "Attribute" in the inspector contextual-menu that appears
 # when the user right-clicks on the attribute of a node in the inspector,
 # and that allows to remove this attribute.
 inspector.menu.removeAttribute.label=Remove Attribute %S
+
+# LOCALIZATION NOTE (inspector.nodePreview.selectNodeLabel):
+# This string is displayed in a tooltip that is shown when hovering over a DOM
+# node preview (e.g. something like "div#foo.bar").
+# DOM node previews can be displayed in places like the animation-inspector, the
+# console or the object inspector.
+# The tooltip invites the user to click on the node in order to select it in the
+# inspector panel.
+inspector.nodePreview.selectNodeLabel=Click to select this node in the Inspector
+
+# LOCALIZATION NOTE (inspector.nodePreview.highlightNodeLabel):
+# This string is displayed in a tooltip that is shown when hovering over a the
+# inspector icon displayed next to a DOM node preview (e.g. next to something
+# like "div#foo.bar").
+# DOM node previews can be displayed in places like the animation-inspector, the
+# console or the object inspector.
+# The tooltip invites the user to click on the icon in order to highlight the
+# node in the page.
+inspector.nodePreview.highlightNodeLabel=Click to highlight this node in the page
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -65,16 +65,17 @@ var AnimationPlayerActor = ActorClass({
    * @param {AnimationsActor} The main AnimationsActor instance
    * @param {AnimationPlayer} The player object returned by getAnimationPlayers
    */
   initialize: function(animationsActor, player) {
     Actor.prototype.initialize.call(this, animationsActor.conn);
 
     this.onAnimationMutation = this.onAnimationMutation.bind(this);
 
+    this.walker = animationsActor.walker;
     this.tabActor = animationsActor.tabActor;
     this.player = player;
     this.node = player.effect.target;
 
     let win = this.node.ownerDocument.defaultView;
     this.styles = win.getComputedStyle(this.node);
 
     // Listen to animation mutations on the node to alert the front when the
@@ -84,17 +85,19 @@ var AnimationPlayerActor = ActorClass({
   },
 
   destroy: function() {
     // Only try to disconnect the observer if it's not already dead (i.e. if the
     // container view hasn't navigated since).
     if (this.observer && !Cu.isDeadWrapper(this.observer)) {
       this.observer.disconnect();
     }
-    this.tabActor = this.player = this.node = this.styles = this.observer = null;
+    this.tabActor = this.player = this.node = this.styles = null;
+    this.observer = this.walker = null;
+
     Actor.prototype.destroy.call(this);
   },
 
   /**
    * Release the actor, when it isn't needed anymore.
    * Protocol.js uses this release method to call the destroy method.
    */
   release: method(function() {}, {release: true}),
@@ -102,16 +105,22 @@ var AnimationPlayerActor = ActorClass({
   form: function(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
 
     let data = this.getCurrentState();
     data.actor = this.actorID;
 
+    // If we know the WalkerActor, and if the animated node is known by it, then
+    // return its corresponding NodeActor ID too.
+    if (this.walker && this.walker.hasNode(this.node)) {
+      data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID;
+    }
+
     return data;
   },
 
   isAnimation: function(player=this.player) {
     return player instanceof this.tabActor.window.CSSAnimation;
   },
 
   isTransition: function(player=this.player) {
@@ -377,16 +386,28 @@ var AnimationPlayerFront = FrontClass(An
     this.state = this.initialState;
   },
 
   destroy: function() {
     Front.prototype.destroy.call(this);
   },
 
   /**
+   * If the AnimationsActor was given a reference to the WalkerActor previously
+   * then calling this getter will return the animation target NodeFront.
+   */
+  get animationTargetNodeFront() {
+    if (!this._form.animationTargetNodeActorID) {
+      return null;
+    }
+
+    return this.conn.getActor(this._form.animationTargetNodeActorID);
+  },
+
+  /**
    * Getter for the initial state of the player. Up to date states can be
    * retrieved by calling the getCurrentState method.
    */
   get initialState() {
     return {
       type: this._form.type,
       startTime: this._form.startTime,
       previousStartTime: this._form.previousStartTime,
@@ -490,28 +511,45 @@ var AnimationsActor = exports.Animations
   },
 
   destroy: function() {
     Actor.prototype.destroy.call(this);
     events.off(this.tabActor, "will-navigate", this.onWillNavigate);
     events.off(this.tabActor, "navigate", this.onNavigate);
 
     this.stopAnimationPlayerUpdates();
-    this.tabActor = this.observer = this.actors = null;
+    this.tabActor = this.observer = this.actors = this.walker = null;
   },
 
   /**
    * Since AnimationsActor doesn't have a protocol.js parent actor that takes
    * care of its lifetime, implementing disconnect is required to cleanup.
    */
   disconnect: function() {
     this.destroy();
   },
 
   /**
+   * Clients can optionally call this with a reference to their WalkerActor.
+   * If they do, then AnimationPlayerActor's forms are going to also include
+   * NodeActor IDs when the corresponding NodeActors do exist.
+   * This, in turns, is helpful for clients to avoid having to go back once more
+   * to the server to get a NodeActor for a particular animation.
+   * @param {WalkerActor} walker
+   */
+  setWalkerActor: method(function(walker) {
+    this.walker = walker;
+  }, {
+    request: {
+      walker: Arg(0, "domwalker")
+    },
+    response: {}
+  }),
+
+  /**
    * Retrieve the list of AnimationPlayerActor actors for currently running
    * animations on a node and its descendants.
    * @param {NodeActor} nodeActor The NodeActor as defined in
    * /devtools/server/actors/inspector
    */
   getAnimationPlayersForNode: method(function(nodeActor) {
     let animations = [
       ...nodeActor.rawNode.getAnimations(),
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -1362,17 +1362,17 @@ var WalkerActor = protocol.ActorClass({
    */
   _onEventListenerChange: function(changesEnum) {
     let changes = changesEnum.enumerate();
     while (changes.hasMoreElements()) {
       let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange);
       let target = current.target;
 
       if (this._refMap.has(target)) {
-        let actor = this._refMap.get(target);
+        let actor = this.getNode(target);
         let mutation = {
           type: "events",
           target: actor.actorID,
           hasEventListeners: actor._hasEventListeners
         };
         this.queueMutation(mutation);
       }
     }
@@ -1464,23 +1464,40 @@ var WalkerActor = protocol.ActorClass({
           this._activePseudoClassLocks.has(actor)) {
         this.clearPseudoClassLocks(actor);
       }
       this._refMap.delete(actor.rawNode);
     }
     protocol.Actor.prototype.unmanage.call(this, actor);
   },
 
-  hasNode: function(node) {
-    return this._refMap.has(node);
+  /**
+   * Determine if the walker has come across this DOM node before.
+   * @param {DOMNode} rawNode
+   * @return {Boolean}
+   */
+  hasNode: function(rawNode) {
+    return this._refMap.has(rawNode);
+  },
+
+  /**
+   * If the walker has come across this DOM node before, then get the
+   * corresponding node actor.
+   * @param {DOMNode} rawNode
+   * @return {NodeActor}
+   */
+  getNode: function(rawNode) {
+    return this._refMap.get(rawNode);
   },
 
   _ref: function(node) {
-    let actor = this._refMap.get(node);
-    if (actor) return actor;
+    let actor = this.getNode(node);
+    if (actor) {
+      return actor;
+    }
 
     actor = new NodeActor(this, node);
 
     // Add the node actor as a child of this walker actor, assigning
     // it an actorID.
     this.manage(actor);
     this._refMap.set(node, actor);
 
@@ -1758,17 +1775,17 @@ var WalkerActor = protocol.ActorClass({
       // Forcing a retained node to go away.
       this._retainedOrphans.delete(node);
     }
 
     let walker = this.getDocumentWalker(node.rawNode);
 
     let child = walker.firstChild();
     while (child) {
-      let childActor = this._refMap.get(child);
+      let childActor = this.getNode(child);
       if (childActor) {
         this.releaseNode(childActor, options);
       }
       child = walker.nextSibling();
     }
 
     node.destroy();
   }, {
@@ -1784,17 +1801,17 @@ var WalkerActor = protocol.ActorClass({
    */
   ensurePathToRoot: function(node, newParents=new Set()) {
     if (!node) {
       return newParents;
     }
     let walker = this.getDocumentWalker(node.rawNode);
     let cur;
     while ((cur = walker.parentNode())) {
-      let parent = this._refMap.get(cur);
+      let parent = this.getNode(cur);
       if (!parent) {
         // This parent didn't exist, so hasn't been seen by the client yet.
         newParents.add(this._ref(cur));
       } else {
         // This parent did exist, so the client knows about it.
         return newParents;
       }
     }
@@ -2937,17 +2954,17 @@ var WalkerActor = protocol.ActorClass({
    */
   onMutations: function(mutations) {
     // Notify any observers that want *all* mutations (even on nodes that aren't
     // referenced).  This is not sent over the protocol so can only be used by
     // scripts running in the server process.
     events.emit(this, "any-mutation");
 
     for (let change of mutations) {
-      let targetActor = this._refMap.get(change.target);
+      let targetActor = this.getNode(change.target);
       if (!targetActor) {
         continue;
       }
       let targetNode = change.target;
       let type = change.type;
       let mutation = {
         type: type,
         target: targetActor.actorID,
@@ -2967,28 +2984,28 @@ var WalkerActor = protocol.ActorClass({
           mutation.newValue = targetNode.nodeValue;
         }
       } else if (type === "childList" || type === "nativeAnonymousChildList") {
         // Get the list of removed and added actors that the client has seen
         // so that it can keep its ownership tree up to date.
         let removedActors = [];
         let addedActors = [];
         for (let removed of change.removedNodes) {
-          let removedActor = this._refMap.get(removed);
+          let removedActor = this.getNode(removed);
           if (!removedActor) {
             // If the client never encountered this actor we don't need to
             // mention that it was removed.
             continue;
           }
           // While removed from the tree, nodes are saved as orphaned.
           this._orphaned.add(removedActor);
           removedActors.push(removedActor.actorID);
         }
         for (let added of change.addedNodes) {
-          let addedActor = this._refMap.get(added);
+          let addedActor = this.getNode(added);
           if (!addedActor) {
             // If the client never encounted this actor we don't need to tell
             // it about its addition for ownership tree purposes - if the
             // client wants to see the new nodes it can ask for children.
             continue;
           }
           // The actor is reconnected to the ownership tree, unorphan
           // it and let the client know so that its ownership tree is up
@@ -3016,17 +3033,17 @@ var WalkerActor = protocol.ActorClass({
       this.rootNode = this.document();
       this.queueMutation({
         type: "newRoot",
         target: this.rootNode.form()
       });
       return;
     }
     let frame = getFrameElement(window);
-    let frameActor = this._refMap.get(frame);
+    let frameActor = this.getNode(frame);
     if (!frameActor) {
       return;
     }
 
     this.queueMutation({
       type: "frameLoad",
       target: frameActor.actorID,
     });
@@ -3071,17 +3088,17 @@ var WalkerActor = protocol.ActorClass({
       this.queueMutation({
         target: this.rootNode.actorID,
         type: "unretained",
         nodes: releasedOrphans
       });
     }
 
     let doc = window.document;
-    let documentActor = this._refMap.get(doc);
+    let documentActor = this.getNode(doc);
     if (!documentActor) {
       return;
     }
 
     if (this.rootDoc === doc) {
       this.rootDoc = null;
       this.rootNode = null;
     }
@@ -3093,17 +3110,17 @@ var WalkerActor = protocol.ActorClass({
 
     let walker = this.getDocumentWalker(doc);
     let parentNode = walker.parentNode();
     if (parentNode) {
       // Send a childList mutation on the frame so that clients know
       // they should reread the children list.
       this.queueMutation({
         type: "childList",
-        target: this._refMap.get(parentNode).actorID,
+        target: this.getNode(parentNode).actorID,
         added: [],
         removed: []
       });
     }
 
     // Need to force a release of this node, because those nodes can't
     // be accessed anymore.
     this.releaseNode(documentActor, { force: true });