DOM MutationObserver vs. CSS Animation vs. Mutation Events 2 (v5)

Revision 5 of this benchmark created on


Preparation HTML

<p id="message"></p>
<div id="root"></div>

<style type="text/css">
@-moz-keyframes nodeInserted {
  from { clip: rect(1px, auto, auto, auto); }
  to { clip: rect(0px, auto, auto, auto); }
}
@-webkit-keyframes nodeInserted {
  from { clip: rect(1px, auto, auto, auto); }
  to { clip: rect(0px, auto, auto, auto); }
}
@-ms-keyframes nodeInserted {
  from { clip: rect(1px, auto, auto, auto); }
  to { clip: rect(0px, auto, auto, auto); }
}
@-o-keyframes nodeInserted {
  from { clip: rect(1px, auto, auto, auto); }
  to { clip: rect(0px, auto, auto, auto); }
}
@keyframes nodeInserted {
  from { clip: rect(1px, auto, auto, auto); }
  to { clip: rect(0px, auto, auto, auto); }
}

.container > span.added {
  -o-animation-duration: 0.001s;
  -ms-animation-duration: 0.001s;
  -moz-animation-duration: 0.001s;
  -webkit-animation-duration: 0.001s;
  animation-duration: 0.001s;
  -o-animation-name: nodeInserted;
  -ms-animation-name: nodeInserted;
  -moz-animation-name: nodeInserted;
  -webkit-animation-name: nodeInserted;
  animation-name: nodeInserted;
}
</style>

<script>
console.log("preparation");
var container, globalDeferred;
var root = document.getElementById("root");

var addTestContainer = function(root) {
  var container = document.createElement("div");
  container.className = "container";
  for (var i = 0; i < 1000; i++) {
    var element = document.createElement("span");
    element.className = "remove";
    container.appendChild(element);
  }
  root.appendChild(container);
  return container;
};

var replaceContentsOf = function(container) {
  container.innerHTML = "<span class=\"added\"></span>";
};

var isAdded = function(node) {
  return node.nodeName.toUpperCase() === "SPAN" &&
    node.classList && node.classList.contains("added");
};

var handleNodeAdded = function() {
  window.globalDeferred.resolve();
  window.globalDeferred = null;
};

var mutationListener = function(mutations) {
  mutations.forEach(function(mutation) {
    if (mutation.type === "childList" && mutation.addedNodes !== null) {
      for (var i = 0; i < mutation.addedNodes.length; i++) {
        if (isAdded(mutation.addedNodes[i]))
          handleNodeAdded();
      }
    }
  });
};

var setMessage = function(messageHtml) {
  document.getElementById("message").innerHTML = messageHtml;
};

var observer;
if (window.MutationObserver) {
  setMessage("Your browser supports <code>MutationObserver</code>.");
  observer = new MutationObserver(mutationListener);
} else if (window.WebKitMutationObserver) {
  setMessage("Your browser supports <code>WebKitMutationObserver</code>.");
  observer = new WebKitMutationObserver(mutationListener);
} else {
  setMessage("Your browser does not support <code>MutationObserver</code>. You will not be able to run these tests.");
}

var animationListener = function(event) {
  if (event.animationName === "nodeInserted" && isAdded(event.target))
    handleNodeAdded();
};

var nodeInsertedListener = function(event) {
  if (isAdded(event.target))
    handleNodeAdded();
};

// Note: cannot use global setup when using ui.benchmarks[i].setup
ui.benchmarks[0].setup = function() {
  console.log("setup[0]");
  for (var i = 0; i < this.benchmark.count; i++)
    addTestContainer(root);
  container = root.firstElementChild;
  observer.observe(root, {
    childList: true,
    subtree: true
  });
};

ui.benchmarks[1].setup = function() {
  console.log("setup[1]");
  for (var i = 0; i < this.benchmark.count; i++)
    addTestContainer(root);
  container = root.firstElementChild;
  root.addEventListener("animationstart", animationListener, false);
  root.addEventListener("MSAnimationStart", animationListener, false);
  root.addEventListener("webkitAnimationStart", animationListener, false);
};

ui.benchmarks[2].setup = function() {
  console.log("setup[2]");
  for (var i = 0; i < this.benchmark.count; i++) {
    var element = addTestContainer(root);
    element.addEventListener("DOMNodeInserted", nodeInsertedListener, false);
  }
  container = root.firstElementChild;
};

ui.benchmarks[0].teardown = function() {
  console.log("teardown[0]");
  observer.disconnect();
  root.innerHTML = "";
};

ui.benchmarks[1].teardown = function() {
  console.log("teardown[1]");
  root.removeEventListener("animationstart", animationListener, false);
  root.removeEventListener("MSAnimationStart", animationListener, false);
  root.removeEventListener("webkitAnimationStart", animationListener, false);
  root.innerHTML = "";
};

ui.benchmarks[2].teardown = function() {
  console.log("teardown[2]");
  for (var i = 0; i < root.childNodes.length; i++) {
    root.childNodes[i].removeEventListener("DOMNodeInserted",
      nodeInsertedListener, false);
  }
  root.innerHTML = "";
};
</script>

Test runner

Ready to run.

Testing in
TestOps/sec
MutationObserver
// async test
window.globalDeferred = deferred;
replaceContentsOf(container);
container = container.nextElementSibling;
ready
animationstart event (CSS)
// async test
window.globalDeferred = deferred;
replaceContentsOf(container);
container = container.nextElementSibling;
ready
DOMNodeInserted event (mutation events)
// async test
window.globalDeferred = deferred;
replaceContentsOf(container);
container = container.nextElementSibling;
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.