Bug 1389384 - Batch l10n mutations to one per paint. r?mossop draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Sat, 16 Dec 2017 21:42:58 -0600
changeset 720631 e3c1f284e19484fe1f8ddc21be8b30bbeefca5ee
parent 717422 05fed903f40f05fd923ba2137696ecc1fa0bafe6
child 746111 1720eed758d6436e99c4608c56ee5159624fd363
push id95593
push userbmo:gandalf@aviary.pl
push dateMon, 15 Jan 2018 22:25:40 +0000
reviewersmossop
bugs1389384
milestone59.0a1
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");
 });