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;
Index: test/browser/elemHideEmulation.js
===================================================================
--- a/test/browser/elemHideEmulation.js
+++ b/test/browser/elemHideEmulation.js
@@ -14,29 +14,46 @@
  * 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 {ElemHideEmulation} = require("../../lib/content/elemHideEmulation");
 
-exports.tearDown = function(callback)
+const REFRESH_INTERVAL = 200;
+
+let testDocument = null;
+
+exports.setUp = function(callback)
 {
-  let styleElements = document.head.getElementsByTagName("style");
-  while (styleElements.length)
-    styleElements[0].parentNode.removeChild(styleElements[0]);
-
-  let child;
-  while (child = document.body.firstChild)
-    child.parentNode.removeChild(child);
+  let iframe = document.createElement("iframe");
+  document.body.appendChild(iframe);
+  testDocument = iframe.contentDocument;
 
   callback();
 };
 
+exports.tearDown = function(callback)
+{
+  let iframe = testDocument.defaultView.frameElement;
+  iframe.parentNode.removeChild(iframe);
+  testDocument = null;
+
+  callback();
+};
+
+function timeout(delay)
+{
+  return new Promise((resolve, reject) =>
+  {
+    window.setTimeout(resolve, delay);
+  });
+}
+
 function unexpectedError(error)
 {
   console.error(error);
   this.ok(false, "Unexpected error: " + error);
 }
 
 function expectHidden(test, element)
 {
@@ -48,56 +65,56 @@
 {
   test.notEqual(window.getComputedStyle(element).display, "none",
                 "The element's display property should not be set to 'none'");
 }
 
 function findUniqueId()
 {
   let id = "elemHideEmulationTest-" + Math.floor(Math.random() * 10000);
-  if (!document.getElementById(id))
+  if (!testDocument.getElementById(id))
     return id;
   return findUniqueId();
 }
 
 function insertStyleRule(rule)
 {
   let styleElement;
-  let styleElements = document.head.getElementsByTagName("style");
+  let styleElements = testDocument.head.getElementsByTagName("style");
   if (styleElements.length)
     styleElement = styleElements[0];
   else
   {
-    styleElement = document.createElement("style");
-    document.head.appendChild(styleElement);
+    styleElement = testDocument.createElement("style");
+    testDocument.head.appendChild(styleElement);
   }
   styleElement.sheet.insertRule(rule, styleElement.sheet.cssRules.length);
 }
 
 // Insert a <div> with a unique id and a CSS rule
 // for the the selector matching the id.
 function createElementWithStyle(styleBlock, parent)
 {
-  let element = document.createElement("div");
+  let element = testDocument.createElement("div");
   element.id = findUniqueId();
   if (!parent)
-    document.body.appendChild(element);
+    testDocument.body.appendChild(element);
   else
     parent.appendChild(element);
   insertStyleRule("#" + element.id + " " + styleBlock);
   return element;
 }
 
 // Create a new ElemHideEmulation instance with @selectors.
 function applyElemHideEmulation(selectors)
 {
   return Promise.resolve().then(() =>
   {
     let elemHideEmulation = new ElemHideEmulation(
-      window,
+      testDocument.defaultView,
       callback =>
       {
         let patterns = [];
         selectors.forEach(selector =>
         {
           patterns.push({selector});
         });
         callback(patterns);
@@ -111,16 +128,17 @@
       },
       elems =>
       {
         for (let elem of elems)
           elem.style.display = "none";
       }
     );
 
+    elemHideEmulation.MIN_INVOCATION_INTERVAL = REFRESH_INTERVAL / 2;
     elemHideEmulation.apply();
     return elemHideEmulation;
   });
 }
 
 exports.testVerbatimPropertySelector = function(test)
 {
   let toHide = createElementWithStyle("{background-color: #000}");
@@ -234,26 +252,26 @@
 {
   let toHide = createElementWithStyle("{}");
   applyElemHideEmulation(
     [":-abp-properties(background-color: rgb(0, 0, 0))"]
   ).then(() =>
   {
     expectVisible(test, toHide);
     insertStyleRule("#" + toHide.id + " {background-color: #000}");
-    return new Promise((resolve, reject) =>
-    {
-      // Re-evaluation will only happen after a few seconds
-      expectVisible(test, toHide);
-      window.setTimeout(() =>
-      {
-        expectHidden(test, toHide);
-        resolve();
-      }, 4000);
-    });
+
+    return timeout(0);
+  }).then(() =>
+  {
+    // Re-evaluation will only happen after a delay
+    expectVisible(test, toHide);
+    return timeout(REFRESH_INTERVAL);
+  }).then(() =>
+  {
+    expectHidden(test, toHide);
   }).catch(unexpectedError.bind(test)).then(() => test.done());
 };
 
 exports.testPseudoClassWithPropBeforeSelector = function(test)
 {
   let parent = createElementWithStyle("{}");
   let child = createElementWithStyle("{background-color: #000}", parent);
   insertStyleRule(`#${child.id}::before {content: "publicite"}`);
@@ -341,34 +359,34 @@
     expectVisible(test, middle);
     expectVisible(test, sibling);
     expectHidden(test, toHide);
   }).catch(unexpectedError.bind(test)).then(() => test.done());
 };
 
 function runTestPseudoClassHasSelectorWithHasAndWithSuffixSibling(test, selector, expectations)
 {
-  document.body.innerHTML = `<div id="parent">
+  testDocument.body.innerHTML = `<div id="parent">
       <div id="middle">
         <div id="middle1"><div id="inside" class="inside"></div></div>
       </div>
       <div id="sibling">
         <div id="tohide">to hide</div>
       </div>
       <div id="sibling2">
         <div id="sibling21"><div id="sibling211" class="inside"></div></div>
       </div>
     </div>`;
   let elems = {
-    parent: document.getElementById("parent"),
-    middle: document.getElementById("middle"),
-    inside: document.getElementById("inside"),
-    sibling: document.getElementById("sibling"),
-    sibling2: document.getElementById("sibling2"),
-    toHide: document.getElementById("tohide")
+    parent: testDocument.getElementById("parent"),
+    middle: testDocument.getElementById("middle"),
+    inside: testDocument.getElementById("inside"),
+    sibling: testDocument.getElementById("sibling"),
+    sibling2: testDocument.getElementById("sibling2"),
+    toHide: testDocument.getElementById("tohide")
   };
 
   insertStyleRule(".inside {}");
 
   applyElemHideEmulation(
     [selector]
   ).then(() =>
   {
@@ -422,33 +440,33 @@
     toHide: true
   };
   runTestPseudoClassHasSelectorWithHasAndWithSuffixSibling(
     test, "div:-abp-has(> body div.inside) + div > div", expectations);
 };
 
 exports.testPseudoClassContains = function(test)
 {
-  document.body.innerHTML = `<div id="parent">
+  testDocument.body.innerHTML = `<div id="parent">
       <div id="middle">
         <div id="middle1"><div id="inside" class="inside"></div></div>
       </div>
       <div id="sibling">
         <div id="tohide">to hide</div>
       </div>
       <div id="sibling2">
         <div id="sibling21"><div id="sibling211" class="inside"></div></div>
       </div>
     </div>`;
-  let parent = document.getElementById("parent");
-  let middle = document.getElementById("middle");
-  let inside = document.getElementById("inside");
-  let sibling = document.getElementById("sibling");
-  let sibling2 = document.getElementById("sibling2");
-  let toHide = document.getElementById("tohide");
+  let parent = testDocument.getElementById("parent");
+  let middle = testDocument.getElementById("middle");
+  let inside = testDocument.getElementById("inside");
+  let sibling = testDocument.getElementById("sibling");
+  let sibling2 = testDocument.getElementById("sibling2");
+  let toHide = testDocument.getElementById("tohide");
 
   applyElemHideEmulation(
     ["#parent div:-abp-contains(to hide)"]
   ).then(() =>
   {
     expectVisible(test, parent);
     expectVisible(test, middle);
     expectVisible(test, inside);
@@ -479,8 +497,102 @@
   applyElemHideEmulation(
     ["div:-abp-has(:-abp-properties(background-color: rgb(0, 0, 0)))"]
   ).then(() =>
   {
     expectVisible(test, child);
     expectHidden(test, parent);
   }).catch(unexpectedError.bind(test)).then(() => test.done());
 };
+
+exports.testDomUpdatesStyle = function(test)
+{
+  let parent = createElementWithStyle("{}");
+  let child = createElementWithStyle("{}", parent);
+  applyElemHideEmulation(
+    ["div:-abp-has(:-abp-properties(background-color: rgb(0, 0, 0)))"]
+  ).then(() =>
+  {
+    expectVisible(test, child);
+    expectVisible(test, parent);
+
+    insertStyleRule("body #" + parent.id + " > div { background-color: #000}");
+    return timeout(0);
+  }).then(() =>
+  {
+    expectVisible(test, child);
+    expectVisible(test, parent);
+    return timeout(REFRESH_INTERVAL);
+  }).then(() =>
+  {
+    expectVisible(test, child);
+    expectHidden(test, parent);
+  }).catch(unexpectedError.bind(test)).then(() => test.done());
+};
+
+exports.testDomUpdatesContent = function(test)
+{
+  let parent = createElementWithStyle("{}");
+  let child = createElementWithStyle("{}", parent);
+  applyElemHideEmulation(
+    ["div > div:-abp-contains(hide me)"]
+  ).then(() =>
+  {
+    expectVisible(test, parent);
+    expectVisible(test, child);
+
+    child.textContent = "hide me";
+    return timeout(0);
+  }).then(() =>
+  {
+    expectVisible(test, parent);
+    expectVisible(test, child);
+    return timeout(REFRESH_INTERVAL);
+  }).then(() =>
+  {
+    expectVisible(test, parent);
+    expectHidden(test, child);
+  }).catch(unexpectedError.bind(test)).then(() => test.done());
+};
+
+exports.testDomUpdatesNewElement = function(test)
+{
+  let parent = createElementWithStyle("{}");
+  let child = createElementWithStyle("{ background-color: #000}", parent);
+  let sibling;
+  let child2;
+  applyElemHideEmulation(
+    ["div:-abp-has(:-abp-properties(background-color: rgb(0, 0, 0)))"]
+  ).then(() =>
+  {
+    expectHidden(test, parent);
+    expectVisible(test, child);
+
+    sibling = createElementWithStyle("{}");
+    return timeout(0);
+  }).then(() =>
+  {
+    expectHidden(test, parent);
+    expectVisible(test, child);
+    expectVisible(test, sibling);
+
+    return timeout(REFRESH_INTERVAL);
+  }).then(() =>
+  {
+    expectHidden(test, parent);
+    expectVisible(test, child);
+    expectVisible(test, sibling);
+
+    child2 = createElementWithStyle("{ background-color: #000}",
+                                    sibling);
+    return timeout(0);
+  }).then(() =>
+  {
+    expectVisible(test, child2);
+    return timeout(REFRESH_INTERVAL);
+  }).then(() =>
+  {
+    expectHidden(test, parent);
+    expectVisible(test, child);
+    expectHidden(test, sibling);
+    expectVisible(test, child2);
+  }).catch(unexpectedError.bind(test)).then(() => test.done());
+};
