Bug 1389384 - Batch l10n mutations to one per paint. r=mossop
authorZibi Braniecki <zbraniecki@mozilla.com>
Sat, 16 Dec 2017 21:42:58 -0600
changeset 451158 e140335f123762045a9a3bfcd31e83736f968150
parent 451157 e3cd1efb911c0fb74e36640b5866300570749a6e
child 451159 196006c746bde8b894252ec1310a4b5124e04c14
push id8543
push userryanvm@gmail.com
push dateTue, 16 Jan 2018 14:33:22 +0000
treeherdermozilla-beta@a6525ed16a32 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmossop
bugs1389384
milestone59.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 1389384 - Batch l10n mutations to one per paint. r=mossop MozReview-Commit-ID: AbclA2lzTfT
intl/l10n/DOMLocalization.jsm
intl/l10n/test/test_domlocalization.js
--- a/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -292,16 +292,21 @@ class DOMLocalization extends Localizati
    *                                              generator over MessageContexts
    * @returns {DOMLocalization}
    */
   constructor(windowElement, resourceIds, generateMessages) {
     super(resourceIds, generateMessages);
 
     // A Set of DOM trees observed by the `MutationObserver`.
     this.roots = new Set();
+    // requestAnimationFrame handler.
+    this.pendingrAF = null;
+    // list of elements pending for translation.
+    this.pendingElements = new Set();
+    this.windowElement = windowElement;
     this.mutationObserver = new windowElement.MutationObserver(
       mutations => this.translateMutations(mutations)
     );
 
     this.observerConfig = {
       attribute: true,
       characterData: false,
       childList: true,
@@ -425,17 +430,17 @@ class DOMLocalization extends Localizati
   /**
    * Translate all roots associated with this `DOMLocalization`.
    *
    * @returns {Promise}
    */
   translateRoots() {
     const roots = Array.from(this.roots);
     return Promise.all(
-      roots.map(root => this.translateFragment(root))
+      roots.map(root => this.translateElements(this.getTranslatables(root)))
     );
   }
 
   /**
    * Pauses the `MutationObserver`.
    *
    * @private
    */
@@ -459,72 +464,71 @@ class DOMLocalization extends Localizati
    * Translate mutations detected by the `MutationObserver`.
    *
    * @private
    */
   translateMutations(mutations) {
     for (const mutation of mutations) {
       switch (mutation.type) {
         case 'attributes':
-          this.translateElement(mutation.target);
+          this.pendingElements.add(mutation.target);
           break;
         case 'childList':
           for (const addedNode of mutation.addedNodes) {
             if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
               if (addedNode.childElementCount) {
-                this.translateFragment(addedNode);
+                for (let element of this.getTranslatables(addedNode)) {
+                  this.pendingElements.add(element);
+                }
               } else if (addedNode.hasAttribute(L10NID_ATTR_NAME)) {
-                this.translateElement(addedNode);
+                this.pendingElements.add(addedNode);
               }
             }
           }
           break;
       }
     }
+
+    // This fragment allows us to coalesce all pending translations into a single
+    // requestAnimationFrame.
+    if (this.pendingElements.size > 0) {
+      if (this.pendingrAF === null) {
+        this.pendingrAF = this.windowElement.requestAnimationFrame(() => {
+          this.translateElements(Array.from(this.pendingElements));
+          this.pendingElements.clear();
+          this.pendingrAF = null;
+        });
+      }
+    }
   }
 
   /**
    * Translate a DOM element or fragment asynchronously using this
    * `DOMLocalization` object.
    *
-   * Manually trigger the translation (or re-translation) of a DOM fragment.
+   * Manually trigger the translation (or re-translation) of a list of elements.
    * Use the `data-l10n-id` and `data-l10n-args` attributes to mark up the DOM
    * with information about which translations to use.
    *
    * Returns a `Promise` that gets resolved once the translation is complete.
    *
-   * @param   {DOMFragment} frag - Element or DocumentFragment to be translated
+   * @param   {Array<Element>} elements - List of elements to be translated
    * @returns {Promise}
    */
-  async translateFragment(frag) {
-    const elements = this.getTranslatables(frag);
+  async translateElements(elements) {
     if (!elements.length) {
       return undefined;
     }
 
     const keys = elements.map(this.getKeysForElement);
     const translations = await this.formatMessages(keys);
     return this.applyTranslations(elements, translations);
   }
 
   /**
-   * Translate a single DOM element asynchronously.
-   *
-   * Returns a `Promise` that gets resolved once the translation is complete.
-   *
-   * @param   {Element} element - HTML element to be translated
-   * @returns {Promise}
-   */
-  async translateElement(element) {
-    const translations =
-      await this.formatMessages([this.getKeysForElement(element)]);
-    return this.applyTranslations([element], translations);
-  }
-
-  /**
    * Applies translations onto elements.
    *
    * @param {Array<Element>} elements
    * @param {Array<Object>}  translations
    * @private
    */
   applyTranslations(elements, translations) {
     this.pauseObserving();
--- a/intl/l10n/test/test_domlocalization.js
+++ b/intl/l10n/test/test_domlocalization.js
@@ -2,14 +2,13 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const { DOMLocalization } =
   Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
 
 add_task(function test_methods_presence() {
   equal(typeof DOMLocalization.prototype.getAttributes, "function");
   equal(typeof DOMLocalization.prototype.setAttributes, "function");
-  equal(typeof DOMLocalization.prototype.translateElement, "function");
-  equal(typeof DOMLocalization.prototype.translateFragment, "function");
+  equal(typeof DOMLocalization.prototype.translateElements, "function");
   equal(typeof DOMLocalization.prototype.connectRoot, "function");
   equal(typeof DOMLocalization.prototype.disconnectRoot, "function");
   equal(typeof DOMLocalization.prototype.translateRoots, "function");
 });