Index: lib/content/elemHideEmulation.js
===================================================================
--- a/lib/content/elemHideEmulation.js
+++ b/lib/content/elemHideEmulation.js
@@ -14,17 +14,18 @@
  * You should have received a copy of the GNU General Public License
  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
  */
 
 "use strict";
 
 const {filterToRegExp, splitSelector} = require("common");
 
-const MIN_INVOCATION_INTERVAL = 3000;
+let MIN_INVOCATION_INTERVAL = 3000;
+const MAX_SYNCHRONOUS_PROCESSING_TIME = 50;
 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i;
 
 /** Return position of node from parent.
  * @param {Node} node the node to find the position of.
  * @return {number} One-based index like for :nth-child(), or 0 on error.
  */
 function positionInParent(node)
 {
@@ -32,16 +33,18 @@
   for (let i = 0; i < children.length; i++)
     if (children[i] == node)
       return i + 1;
   return 0;
 }
 
 function makeSelector(node, selector)
 {
+  if (node == null)
+    return null;
   if (!node.parentElement)
   {
     let newSelector = ":root";
     if (selector)
       newSelector += " > " + selector;
     return newSelector;
   }
   let idx = positionInParent(node);
@@ -123,17 +126,26 @@
 {
   if (index >= chain.length)
   {
     yield prefix;
     return;
   }
   for (let [selector, element] of
        chain[index].getSelectors(prefix, subtree, styles))
-    yield* evaluate(chain, index + 1, selector, element, styles);
+  {
+    if (selector == null)
+      yield null;
+    else
+      yield* evaluate(chain, index + 1, selector, element, styles);
+  }
+  // Just in case the getSelectors() generator above had to run some heavy
+  // document.querySelectorAll() call which didn't produce any results, make
+  // sure there is at least one point where execution can pause.
+  yield null;
 }
 
 function PlainSelector(selector)
 {
   this._selector = selector;
 }
 
 PlainSelector.prototype = {
@@ -183,28 +195,34 @@
     let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
         prefix + "*" : prefix;
     let elements = subtree.querySelectorAll(actualPrefix);
     for (let element of elements)
     {
       let iter = evaluate(this._innerSelectors, 0, "", element, styles);
       for (let selector of iter)
       {
+        if (selector == null)
+        {
+          yield null;
+          continue;
+        }
         if (relativeSelectorRegexp.test(selector))
           selector = ":scope" + selector;
         try
         {
           if (element.querySelector(selector))
             yield element;
         }
         catch (e)
         {
           // :scope isn't supported on Edge, ignore error caused by it.
         }
       }
+      yield null;
     }
   }
 };
 
 function ContainsSelector(textContent)
 {
   this._text = textContent;
 }
@@ -218,19 +236,24 @@
       yield [makeSelector(element, ""), subtree];
   },
 
   *getElements(prefix, subtree, stylesheet)
   {
     let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ?
         prefix + "*" : prefix;
     let elements = subtree.querySelectorAll(actualPrefix);
+
     for (let element of elements)
+    {
       if (element.textContent.includes(this._text))
         yield element;
+      else
+        yield null;
+    }
   }
 };
 
 function PropsSelector(propertyExpression)
 {
   let regexpString;
   if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
       propertyExpression[propertyExpression.length - 1] == "/")
@@ -268,23 +291,30 @@
 
   *getSelectors(prefix, subtree, styles)
   {
     for (let selector of this.findPropsSelectors(styles, prefix, this._regexp))
       yield [selector, subtree];
   }
 };
 
+function isSelectorHidingOnlyPattern(pattern)
+{
+  return pattern.selectors.some(s => s.preferHideWithSelector) &&
+    !pattern.selectors.some(s => s.requiresHiding);
+}
+
 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc,
                            hideElemsFunc)
 {
   this.window = window;
   this.getFiltersFunc = getFiltersFunc;
   this.addSelectorsFunc = addSelectorsFunc;
   this.hideElemsFunc = hideElemsFunc;
+  this.observer = new window.MutationObserver(this.observe.bind(this));
 }
 
 ElemHideEmulation.prototype = {
   isSameOrigin(stylesheet)
   {
     try
     {
       return new URL(stylesheet.href).origin == this.window.location.origin;
@@ -357,30 +387,28 @@
         new SyntaxError("Failed to parse Adblock Plus " +
                         `selector ${selector}, can't ` +
                         "have a lonely :-abp-contains()."));
       return null;
     }
     return selectors;
   },
 
-  _lastInvocation: 0,
-
   /**
    * Processes the current document and applies all rules to it.
    * @param {CSSStyleSheet[]} [stylesheets]
    *    The list of new stylesheets that have been added to the document and
    *    made reprocessing necessary. This parameter shouldn't be passed in for
    *    the initial processing, all of document's stylesheets will be considered
    *    then and all rules, including the ones not dependent on styles.
+   * @param {function} [done]
+   *    Callback to call when done.
    */
-  addSelectors(stylesheets)
+  _addSelectors(stylesheets, done)
   {
-    this._lastInvocation = Date.now();
-
     let selectors = [];
     let selectorFilters = [];
 
     let elements = [];
     let elementFilters = [];
 
     let cssStyles = [];
 
@@ -407,72 +435,156 @@
         if (rule.type != rule.STYLE_RULE)
           continue;
 
         cssStyles.push(stringifyStyle(rule));
       }
     }
 
     let {document} = this.window;
-    for (let pattern of this.patterns)
+
+    let patterns = this.patterns.slice();
+    let pattern = null;
+    let generator = null;
+
+    let processPatterns = () =>
     {
-      if (stylesheetOnlyChange &&
-          !pattern.selectors.some(selector => selector.dependsOnStyles))
-      {
-        continue;
-      }
+      let cycleStart = this.window.performance.now();
 
-      for (let selector of evaluate(pattern.selectors,
-                                    0, "", document, cssStyles))
+      if (!pattern)
       {
-        if (pattern.selectors.some(s => s.preferHideWithSelector) &&
-            !pattern.selectors.some(s => s.requiresHiding))
+        if (!patterns.length)
         {
-          selectors.push(selector);
-          selectorFilters.push(pattern.text);
+          this.addSelectorsFunc(selectors, selectorFilters);
+          this.hideElemsFunc(elements, elementFilters);
+          if (typeof done == "function")
+            done();
+          return;
+        }
+
+        pattern = patterns.shift();
+
+        if (stylesheetOnlyChange &&
+            !pattern.selectors.some(selector => selector.dependsOnStyles))
+        {
+          pattern = null;
+          return processPatterns();
         }
-        else
+        generator = evaluate(pattern.selectors, 0, "", document, cssStyles);
+      }
+      for (let selector of generator)
+      {
+        if (selector != null)
         {
-          for (let element of document.querySelectorAll(selector))
+          if (isSelectorHidingOnlyPattern(pattern))
           {
-            elements.push(element);
-            elementFilters.push(pattern.text);
+            selectors.push(selector);
+            selectorFilters.push(pattern.text);
+          }
+          else
+          {
+            for (let element of document.querySelectorAll(selector))
+            {
+              elements.push(element);
+              elementFilters.push(pattern.text);
+            }
           }
         }
+        if (this.window.performance.now() -
+            cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME)
+        {
+          this.window.setTimeout(processPatterns, 0);
+          return;
+        }
       }
-    }
+      pattern = null;
+      return processPatterns();
+    };
 
-    this.addSelectorsFunc(selectors, selectorFilters);
-    this.hideElemsFunc(elements, elementFilters);
+    processPatterns();
+  },
+
+  // This property is only used in the tests
+  // to shorten the invocation interval
+  get MIN_INVOCATION_INTERVAL()
+  {
+    return MIN_INVOCATION_INTERVAL;
+  },
+
+  set MIN_INVOCATION_INTERVAL(interval)
+  {
+    MIN_INVOCATION_INTERVAL = interval;
   },
 
-  _stylesheetQueue: null,
+  _filteringInProgress: false,
+  _lastInvocation: -MIN_INVOCATION_INTERVAL,
+  _scheduledProcessing: null,
+
+  /**
+   * Re-run filtering either immediately or queued.
+   * @param {CSSStyleSheet[]} [stylesheets]
+   *    new stylesheets to be processed. This parameter should be omitted
+   *    for DOM modification (full reprocessing required).
+   */
+  queueFiltering(stylesheets)
+  {
+    let completion = () =>
+    {
+      this._lastInvocation = this.window.performance.now();
+      this._filteringInProgress = false;
+      if (this._scheduledProcessing)
+      {
+        let newStylesheets = this._scheduledProcessing.stylesheets;
+        this._scheduledProcessing = null;
+        this.queueFiltering(newStylesheets);
+      }
+    };
+
+    if (this._scheduledProcessing)
+    {
+      if (!stylesheets)
+        this._scheduledProcessing.stylesheets = null;
+      else if (this._scheduledProcessing.stylesheets)
+        this._scheduledProcessing.stylesheets.push(...stylesheets);
+    }
+    else if (this._filteringInProgress)
+    {
+      this._scheduledProcessing = {stylesheets};
+    }
+    else if (this.window.performance.now() -
+             this._lastInvocation < MIN_INVOCATION_INTERVAL)
+    {
+      this._scheduledProcessing = {stylesheets};
+      this.window.setTimeout(() =>
+      {
+        let newStylesheets = this._scheduledProcessing.stylesheets;
+        this._filteringInProgress = true;
+        this._scheduledProcessing = null;
+        this._addSelectors(newStylesheets, completion);
+      },
+      MIN_INVOCATION_INTERVAL -
+      (this.window.performance.now() - this._lastInvocation));
+    }
+    else
+    {
+      this._filteringInProgress = true;
+      this._addSelectors(stylesheets, completion);
+    }
+  },
 
   onLoad(event)
   {
     let stylesheet = event.target.sheet;
     if (stylesheet)
-    {
-      if (!this._stylesheetQueue &&
-          Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL)
-      {
-        this._stylesheetQueue = [];
-        this.window.setTimeout(() =>
-        {
-          let stylesheets = this._stylesheetQueue;
-          this._stylesheetQueue = null;
-          this.addSelectors(stylesheets);
-        }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation));
-      }
+      this.queueFiltering([stylesheet]);
+  },
 
-      if (this._stylesheetQueue)
-        this._stylesheetQueue.push(stylesheet);
-      else
-        this.addSelectors([stylesheet]);
-    }
+  observe(mutations)
+  {
+    this.queueFiltering();
   },
 
   apply()
   {
     this.getFiltersFunc(patterns =>
     {
       this.patterns = [];
       for (let pattern of patterns)
@@ -480,16 +592,25 @@
         let selectors = this.parseSelector(pattern.selector);
         if (selectors != null && selectors.length > 0)
           this.patterns.push({selectors, text: pattern.text});
       }
 
       if (this.patterns.length > 0)
       {
         let {document} = this.window;
-        this.addSelectors();
+        this.queueFiltering();
+        this.observer.observe(
+          document,
+          {
+            childList: true,
+            attributes: true,
+            characterData: true,
+            subtree: true
+          }
+        );
         document.addEventListener("load", this.onLoad.bind(this), true);
       }
     });
   }
 };
 
 exports.ElemHideEmulation = ElemHideEmulation;
