Bug 1441935 - Modifications on top of the generated MozBrowser Custom Element r=mconley
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 10 Jan 2019 01:45:43 +0000
changeset 510300 bd38d0bde8a3654bd1d727ebb5f301535b6da273
parent 510299 4e943c6de64e19c107902ca41e5d18a6ec9d1395
child 510301 b342c8e8306e9ab655838f211e18eb65479b29c5
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1441935
milestone66.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 1441935 - Modifications on top of the generated MozBrowser Custom Element r=mconley In order to make the history easier to navigate, this changeset includes the modifications required to make <xul:browser> actually work as a Custom Element, and switches the app to use it instead of the XBL browser. Differential Revision: https://phabricator.services.mozilla.com/D14911
browser/base/content/browser-fullZoom.js
browser/base/content/tabbrowser.js
devtools/client/responsive.html/browser/swap.js
devtools/client/responsive.html/browser/tunnel.js
devtools/server/actors/network-monitor/network-observer.js
dom/ipc/tests/process_error.xul
gfx/layers/apz/src/AutoscrollAnimation.cpp
mobile/android/app/mobile.js
mobile/android/chrome/content/browser.js
toolkit/components/processsingleton/CustomElementsListener.jsm
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/widgets/browser-custom-element.js
toolkit/content/xul.css
toolkit/modules/E10SUtils.jsm
--- a/browser/base/content/browser-fullZoom.js
+++ b/browser/base/content/browser-fullZoom.js
@@ -341,17 +341,17 @@ var FullZoom = {
     if (!this.siteSpecific || gInPrintPreviewMode) {
       this._executeSoon(aCallback);
       return;
     }
 
     // The browser is sometimes half-destroyed because this method is called
     // by content pref service callbacks, which themselves can be called at any
     // time, even after browsers are closed.
-    if (!aBrowser.parentNode || aBrowser.isSyntheticDocument) {
+    if (!aBrowser.mInitialized || aBrowser.isSyntheticDocument) {
       this._executeSoon(aCallback);
       return;
     }
 
     if (aValue !== undefined) {
       ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue));
       this._ignorePendingZoomAccesses(aBrowser);
       this._executeSoon(aCallback);
@@ -429,17 +429,17 @@ var FullZoom = {
     return {
       token: map.get(browser),
       get isCurrent() {
         // At this point, the browser may have been destructed and unbound but
         // its outer ID not removed from the map because outer-window-destroyed
         // hasn't been received yet.  In that case, the browser is unusable, it
         // has no properties, so return false.  Check for this case by getting a
         // property, say, docShell.
-        return map.get(browser) === this.token && browser.parentNode;
+        return map.get(browser) === this.token && browser.mInitialized;
       },
     };
   },
 
   /**
    * Returns the browser that the supplied zoom event is associated with.
    * @param event  The ZoomChangeUsingMouseWheel event.
    * @return  The associated browser element, if one exists, otherwise null.
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -108,17 +108,17 @@ window._gBrowser = {
    * Binding from browser to tab
    */
   _tabForBrowser: new WeakMap(),
 
   _preloadedBrowser: null,
 
   /**
    * `_createLazyBrowser` will define properties on the unbound lazy browser
-   * which correspond to properties defined in XBL which will be bound to
+   * which correspond to properties defined in MozBrowser which will be bound to
    * the browser when it is inserted into the document.  If any of these
    * properties are accessed by consumers, `_insertBrowser` is called and
    * the browser is inserted to ensure that things don't break.  This list
    * provides the names of properties that may be called while the browser
    * is in its unbound (lazy) state.
    */
   _browserBindingProperties: [
     "canGoBack", "canGoForward", "goBack", "goForward", "permitUnload",
@@ -3088,24 +3088,24 @@ window._gBrowser = {
     this._tabFilters.delete(aTab);
     this._tabListeners.delete(aTab);
 
     var browser = this.getBrowserForTab(aTab);
 
     if (aTab.linkedPanel) {
       this._outerWindowIDBrowserMap.delete(browser.outerWindowID);
 
-      // Because of the way XBL works (fields just set JS
-      // properties on the element) and the code we have in place
+      // Because of the fact that we are setting JS properties on
+      // the browser elements, and we have code in place
       // to preserve the JS objects for any elements that have
       // JS properties set on them, the browser element won't be
       // destroyed until the document goes away.  So we force a
       // cleanup ourselves.
-      // This has to happen before we remove the child so that the
-      // XBL implementation of nsIObserver still works.
+      // This has to happen before we remove the child since functions
+      // like `getBrowserContainer` expect the browser to be parented.
       browser.destroy();
     }
 
     var wasPinned = aTab.pinned;
 
     // Remove the tab ...
     aTab.remove();
 
--- a/devtools/client/responsive.html/browser/swap.js
+++ b/devtools/client/responsive.html/browser/swap.js
@@ -35,17 +35,17 @@ function debug(msg) {
 function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
   let browserWindow = tab.ownerGlobal;
   let gBrowser = browserWindow.gBrowser;
   let innerBrowser;
   let tunnel;
 
   // Dispatch a custom event each time the _viewport content_ is swapped from one browser
   // to another.  DevTools server code uses this to follow the content if there is an
-  // active DevTools connection.  While browser.xml does dispatch it's own SwapDocShells
+  // active DevTools connection.  While browser.js does dispatch it's own SwapDocShells
   // event, this one is easier for DevTools to follow because it's only emitted once per
   // transition, instead of twice like SwapDocShells.
   const dispatchDevToolsBrowserSwap = (from, to) => {
     const CustomEvent = browserWindow.CustomEvent;
     const event = new CustomEvent("DevTools:BrowserSwap", {
       detail: to,
       bubbles: true,
     });
@@ -416,17 +416,17 @@ function addXULBrowserDecorations(browse
         return browser._outerWindowID;
       },
       configurable: true,
       enumerable: true,
     });
   }
 
   // It's not necessary for these to actually do anything.  These properties are
-  // swapped between browsers in browser.xml's `swapDocShells`, and then their
+  // swapped between browsers in browser.js's `swapDocShells`, and then their
   // `swapBrowser` methods are called, so we define them here for that to work
   // without errors.  During the swap process above, these will move from the
   // the new inner browser to the original tab's browser (step 4) and then to
   // the temporary container tab's browser (step 7), which is then closed.
   if (browser._remoteWebNavigationImpl == undefined) {
     browser._remoteWebNavigationImpl = {
       swapBrowser() {},
     };
--- a/devtools/client/responsive.html/browser/tunnel.js
+++ b/devtools/client/responsive.html/browser/tunnel.js
@@ -14,17 +14,17 @@ const FRAME_LOADER = Symbol("devtools/re
 // Export for use in tests.
 exports.OUTER_FRAME_LOADER_SYMBOL = FRAME_LOADER;
 
 function debug(msg) {
   // console.log(msg);
 }
 
 /**
- * Properties swapped between browsers by browser.xml's `swapDocShells`.
+ * Properties swapped between browsers by browser.js's `swapDocShells`.
  */
 const SWAPPED_BROWSER_STATE = [
   "_remoteFinder",
   "_securityUI",
   "_documentURI",
   "_documentContentType",
   "_contentTitle",
   "_characterSet",
@@ -59,18 +59,18 @@ const PROPERTIES_FROM_BROWSER_WINDOW = [
 
 /**
  * This module takes an "outer" <xul:browser> from a browser tab as described by
  * Firefox's tabbrowser.xml and wires it up to an "inner" <iframe mozbrowser>
  * browser element containing arbitrary page content of interest.
  *
  * The inner <iframe mozbrowser> element is _just_ the page content.  It is not
  * enough to to replace <xul:browser> on its own.  <xul:browser> comes along
- * with lots of associated functionality via XBL binding defined for such
- * elements in browser.xml, and the Firefox UI depends on these various things
+ * with lots of associated functionality via a Custom Element defined for such
+ * elements in browser.js, and the Firefox UI depends on these various things
  * to make the UI function.
  *
  * By mapping various methods, properties, and messages from the outer browser
  * to the inner browser, we can control the content inside the inner browser
  * using the standard Firefox UI elements for navigation, reloading, and more.
  *
  * The approaches used in this module were chosen to avoid needing changes to
  * the core browser for this specialized use case.  If we start to increase
@@ -98,19 +98,19 @@ function tunnelToInnerBrowser(outer, inn
         throw new Error("The outer browser must be non-remote.");
       }
       if (!inner.isRemoteBrowser) {
         throw new Error("The inner browser must be remote.");
       }
 
       // Various browser methods access the `frameLoader` property, including:
       //   * `saveBrowser` from contentAreaUtils.js
-      //   * `docShellIsActive` from browser.xml
-      //   * `hasContentOpener` from browser.xml
-      //   * `preserveLayers` from browser.xml
+      //   * `docShellIsActive` from browser.js
+      //   * `hasContentOpener` from browser.js
+      //   * `preserveLayers` from browser.js
       //   * `receiveMessage` from SessionStore.jsm
       // In general, these methods are interested in the `frameLoader` for the content,
       // so we redirect them to the inner browser's `frameLoader`.
       outer[FRAME_LOADER] = outer.frameLoader;
       Object.defineProperty(outer, "frameLoader", {
         get() {
           const stack = getStack();
           // One exception is `receiveMessage` from SessionStore.jsm.  SessionStore
@@ -148,22 +148,22 @@ function tunnelToInnerBrowser(outer, inn
       // copy the content's `permanentKey` up to the outer browser.
       debug("Copy inner permanentKey to outer browser");
       outer.permanentKey = inner.permanentKey;
 
       // Replace the outer browser's native messageManager with a message manager tunnel
       // which we can use to route messages of interest to the inner browser instead.
       // Note: The _actual_ messageManager accessible from
       // `browser.frameLoader.messageManager` is not overridable and is left unchanged.
-      // Only the XBL getter `browser.messageManager` is overridden.  Browser UI code
-      // always uses this getter instead of `browser.frameLoader.messageManager` directly,
+      // Only the Custom Element getter `browser.messageManager` is overridden. This
+      // getter is always used instead of `browser.frameLoader.messageManager` directly,
       // so this has the effect of overriding the message manager for browser UI code.
       mmTunnel = new MessageManagerTunnel(outer, inner);
 
-      // Clear out any cached state that references the XBL binding's non-remote state,
+      // Clear out any cached state that references the Custom Element's non-remote state,
       // such as form fill controllers.  Otherwise they will remain in place and leak the
       // outer docshell.
       outer.destroy();
 
       // We are tunneling to an inner browser with a specific remoteness, so it is simpler
       // for the logic of the browser UI to assume this tab has taken on that remoteness,
       // even though it's not true.  Since the actions the browser UI performs are sent
       // down to the inner browser by this tunnel, the tab's remoteness effectively is the
@@ -188,19 +188,19 @@ function tunnelToInnerBrowser(outer, inn
       // remote browser binding creates.  We do not care about it's original value
       // because stop() will remove the browser binding and these will no longer bee
       // used.
       const webNavigation = new BrowserElementWebNavigation(inner);
       webNavigation.copyStateFrom(inner._remoteWebNavigationImpl);
       outer._remoteWebNavigation = webNavigation;
       outer._remoteWebNavigationImpl = webNavigation;
 
-      // Now that we've flipped to the remote browser XBL binding, add `progressListener`
+      // Now that we've flipped to the remote browser mode, add `progressListener`
       // onto the remote version of `webProgress`.  Normally tabbrowser.xml does this step
-      // when it creates a new browser, etc.  Since we manually changed the XBL binding
+      // when it creates a new browser, etc.  Since we manually changed the mode
       // above, it caused a fresh webProgress object to be created which does not have any
       // listeners added.  So, we get the listener that gBrowser is using for the tab and
       // reattach it here.
       const tab = gBrowser.getTabForBrowser(outer);
       const filteredProgressListener = gBrowser._tabFilters.get(tab);
       outer.webProgress.addProgressListener(filteredProgressListener);
 
       // Add the inner browser to tabbrowser's WeakMap from browser to tab.  This assists
@@ -300,17 +300,17 @@ function tunnelToInnerBrowser(outer, inn
       }
 
       // Remove the inner browser from the WeakMap from browser to tab.
       gBrowser._tabForBrowser.delete(inner);
 
       // Remove the progress listener we added manually.
       outer.webProgress.removeProgressListener(filteredProgressListener);
 
-      // Reset the XBL binding back to the original state.
+      // Reset the Custom Element back to the original state.
       outer.destroy();
 
       // Reset @remote since this is now back to a regular, non-remote browser
       outer.setAttribute("remote", "false");
       outer.removeAttribute("remoteType");
       outer.construct();
 
       // Delete browser window properties exposed on content's owner global
@@ -383,17 +383,17 @@ MessageManagerTunnel.prototype = {
   OVERRIDDEN_METHODS: [
     "loadFrameScript",
     "addMessageListener",
     "removeMessageListener",
     "sendAsyncMessage",
   ],
 
   OUTER_TO_INNER_MESSAGES: [
-    // Messages sent from remote-browser.xml
+    // Messages sent from browser.js
     "Browser:PurgeSessionHistory",
     "InPermitUnload",
     "PermitUnload",
     // Messages sent from browser.js
     "Browser:Reload",
     "PageStyle:Disable",
     "PageStyle:Switch",
     // Messages sent from SelectParentHelper.jsm
@@ -417,17 +417,17 @@ MessageManagerTunnel.prototype = {
     "PageStyle:StyleSheets",
     // Messages sent to RemoteWebProgress.jsm
     "Content:LoadURIResult",
     "Content:LocationChange",
     "Content:ProgressChange",
     "Content:SecurityChange",
     "Content:StateChange",
     "Content:StatusChange",
-    // Messages sent to remote-browser.xml
+    // Messages sent to browser.js
     "DOMTitleChanged",
     "ImageDocumentLoaded",
     "Forms:ShowDropDown",
     "Forms:HideDropDown",
     "InPermitUnload",
     "PermitUnload",
     // Messages sent to tabbrowser.xml
     "contextmenu",
@@ -435,17 +435,17 @@ MessageManagerTunnel.prototype = {
     "Forms:UpdateDropDown",
     // Messages sent to SessionStore.jsm
     "SessionStore:update",
     // Messages sent to BrowserTestUtils.jsm
     "browser-test-utils:loadEvent",
   ],
 
   OUTER_TO_INNER_MESSAGE_PREFIXES: [
-    // Messages sent from browser.xml
+    // Messages sent from browser.js
     "Autoscroll:",
     // Messages sent from nsContextMenu.js
     "ContextMenu:",
     // Messages sent from DevTools
     "debug:",
     // Messages sent from findbar.xml
     "Findbar:",
     // Messages sent from RemoteFinder.jsm
@@ -458,17 +458,17 @@ MessageManagerTunnel.prototype = {
     "PageInfo:",
     // Messages sent from printUtils.js
     "Printing:",
     // Messages sent from viewSourceUtils.js
     "ViewSource:",
   ],
 
   INNER_TO_OUTER_MESSAGE_PREFIXES: [
-    // Messages sent to browser.xml
+    // Messages sent to browser.js
     "Autoscroll:",
     // Messages sent to nsContextMenu.js
     "ContextMenu:",
     // Messages sent to DevTools
     "debug:",
     // Messages sent to findbar.xml
     "Findbar:",
     // Messages sent to RemoteFinder.jsm
--- a/devtools/server/actors/network-monitor/network-observer.js
+++ b/devtools/server/actors/network-monitor/network-observer.js
@@ -81,17 +81,17 @@ function matchRequest(channel, filters) 
     const topFrame = NetworkHelper.getTopFrameForRequest(channel);
     // topFrame is typically null for some chrome requests like favicons
     if (topFrame) {
       try {
         if (topFrame.outerWindowID == filters.outerWindowID) {
           return true;
         }
       } catch (e) {
-        // outerWindowID getter from browser.xml (non-remote <xul:browser>) may
+        // outerWindowID getter from browser.js (non-remote <xul:browser>) may
         // throw when closing a tab while resources are still loading.
       }
     }
   }
 
   return false;
 }
 exports.matchRequest = matchRequest;
--- a/dom/ipc/tests/process_error.xul
+++ b/dom/ipc/tests/process_error.xul
@@ -26,12 +26,16 @@
       }
 
       Services.obs.removeObserver(crashObserver, 'ipc:content-shutdown');
       done();
     }
 
     Services.obs.addObserver(crashObserver, 'ipc:content-shutdown');
 
-    BrowserTestUtils.crashBrowser(document.getElementById('thebrowser'), true, false);
+    // Allow the browser to get connected before using the messageManager to cause
+    // a crash:
+    addEventListener("DOMContentLoaded", () => {
+      BrowserTestUtils.crashBrowser(document.getElementById('thebrowser'), true, false);
+    });
   ]]></script>
 
 </window>
--- a/gfx/layers/apz/src/AutoscrollAnimation.cpp
+++ b/gfx/layers/apz/src/AutoscrollAnimation.cpp
@@ -71,17 +71,17 @@ bool AutoscrollAnimation::DoSample(Frame
   // An autoscroll animation never ends of its own accord.
   // It can be stopped in response to various input events, in which case
   // AsyncPanZoomController::StopAutoscroll() will stop it via
   // CancelAnimation().
   return true;
 }
 
 void AutoscrollAnimation::Cancel(CancelAnimationFlags aFlags) {
-  // The cancellation was initiated by browser.xml, so there's no need to
+  // The cancellation was initiated by browser.js, so there's no need to
   // notify it.
   if (aFlags & TriggeredExternally) {
     return;
   }
 
   if (RefPtr<GeckoContentController> controller =
           mApzc.GetGeckoContentController()) {
     controller->CancelAutoscroll(mApzc.GetGuid());
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #filter substitution
 
-// For browser.xml binding
+// For browser.js element
 //
 // cacheRatio* is a ratio that determines the amount of pixels to cache. The
 // ratio is multiplied by the viewport width or height to get the displayport's
 // width or height, respectively.
 //
 // (divide integer value by 1000 to get the ratio)
 //
 // For instance: cachePercentageWidth is 1500
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -5373,17 +5373,17 @@ var XPInstallObserver = {
   },
 
   hideRestartPrompt: function() {
     NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
   }
 };
 
 /**
- * Handler for blocked popups, triggered by DOMUpdateBlockedPopups events in browser.xml
+ * Handler for blocked popups, triggered by DOMUpdateBlockedPopups events in browser.js
  */
 var PopupBlockerObserver = {
   onUpdateBlockedPopups: function onUpdateBlockedPopups(aEvent) {
     let browser = BrowserApp.selectedBrowser;
     if (aEvent.originalTarget != browser)
       return;
 
     if (!browser.blockedPopups)
--- a/toolkit/components/processsingleton/CustomElementsListener.jsm
+++ b/toolkit/components/processsingleton/CustomElementsListener.jsm
@@ -8,15 +8,16 @@ ChromeUtils.import("resource://gre/modul
 // Set up Custom Elements for XUL and XHTML documents before anything else
 // happens. Anything loaded here should be considered part of core XUL functionality.
 // Any window-specific elements can be registered via <script> tags at the
 // top of individual documents.
 Services.obs.addObserver({
   observe(doc) {
     if (doc.nodePrincipal.isSystemPrincipal && (
       doc.contentType == "application/vnd.mozilla.xul+xml" ||
-      doc.contentType == "application/xhtml+xml"
+      doc.contentType == "application/xhtml+xml" ||
+      doc.contentType == "text/html"
     )) {
       Services.scriptloader.loadSubScript(
         "chrome://global/content/customElements.js", doc.ownerGlobal);
     }
   },
 }, "document-element-inserted");
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -285,16 +285,20 @@ MozElements.BaseControl = class BaseCont
 MozXULElement.implementCustomInterface(MozElements.BaseControl,
                                        [Ci.nsIDOMXULControlElement]);
 
 // Attach the base class to the window so other scripts can use it:
 window.MozElementMixin = MozElementMixin;
 window.MozXULElement = MozXULElement;
 window.MozElements = MozElements;
 
+customElements.setElementCreationCallback("browser", () => {
+  Services.scriptloader.loadSubScript("chrome://global/content/elements/browser-custom-element.js", window);
+});
+
 // For now, don't load any elements in the extension dummy document.
 // We will want to load <browser> when that's migrated (bug 1441935).
 const isDummyDocument = document.documentURI == "chrome://extensions/content/dummy.xul";
 if (!isDummyDocument) {
   for (let script of [
     "chrome://global/content/elements/general.js",
     "chrome://global/content/elements/notificationbox.js",
     "chrome://global/content/elements/radio.js",
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -85,16 +85,17 @@ toolkit.jar:
 *  content/global/bindings/textbox.xml         (widgets/textbox.xml)
    content/global/bindings/timekeeper.js       (widgets/timekeeper.js)
    content/global/bindings/timepicker.js       (widgets/timepicker.js)
    content/global/bindings/toolbar.xml         (widgets/toolbar.xml)
    content/global/bindings/toolbarbutton.xml   (widgets/toolbarbutton.xml)
    content/global/bindings/tree.xml            (widgets/tree.xml)
    content/global/bindings/videocontrols.xml   (widgets/videocontrols.xml)
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
+   content/global/elements/browser-custom-element.js          (widgets/browser-custom-element.js)
    content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
    content/global/elements/editor.js           (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
    content/global/elements/notificationbox.js  (widgets/notificationbox.js)
    content/global/elements/pluginProblem.js    (widgets/pluginProblem.js)
    content/global/elements/radio.js            (widgets/radio.js)
    content/global/elements/richlistbox.js      (widgets/richlistbox.js)
--- a/toolkit/content/widgets/browser-custom-element.js
+++ b/toolkit/content/widgets/browser-custom-element.js
@@ -1,47 +1,77 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
-  * License, v. 2.0. If a copy of the MPL was not distributed with this
-  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-  /* eslint-disable */
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // This is loaded into all XUL windows. Wrap in a block to prevent
 // leaking to window scope.
 {
 
-class MozBrowser extends MozXULElement {
+const elementsToDestroyOnUnload = new Set();
+
+window.addEventListener("unload", () => {
+  for (let element of elementsToDestroyOnUnload.values()) {
+    element.destroy();
+  }
+  elementsToDestroyOnUnload.clear();
+}, { mozSystemGroup: true, once: true });
+
+class MozBrowser extends MozElementMixin(XULFrameElement) {
+  static get observedAttributes() {
+    return ["remote"];
+  }
+
+  attributeChangedCallback(name, oldValue, newValue) {
+    // When we have already been set up via connectedCallback and the
+    // and the [remote] value changes, we need to start over. This used
+    // to happen due to a XBL binding change.
+    if (name === "remote" && oldValue != newValue && this.isConnectedAndReady) {
+      this.destroy();
+      this.construct();
+    }
+  }
+
   constructor() {
     super();
 
+    this.onPageHide = this.onPageHide.bind(this);
+
+    /**
+     * These are managed by the tabbrowser:
+     */
+    this.droppedLinkHandler = null;
+    this.mIconURL = null;
+    this.lastURI = null;
+
     this.addEventListener("keypress", (event) => {
       if (event.keyCode != KeyEvent.DOM_VK_F7) {
         return;
       }
 
-      if (event.defaultPrevented || !event.isTrusted)
+      if (event.defaultPrevented || !event.isTrusted) {
         return;
+      }
 
       const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled";
       const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
       const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
 
       var isEnabled = this.mPrefs.getBoolPref(kPrefShortcutEnabled);
       if (!isEnabled)
         return;
 
       // Toggle browse with caret mode
       var browseWithCaretOn = this.mPrefs.getBoolPref(kPrefCaretBrowsingOn, false);
       var warn = this.mPrefs.getBoolPref(kPrefWarnOnEnable, true);
       if (warn && !browseWithCaretOn) {
         var checkValue = { value: false };
-        var promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"]
-          .getService(Ci.nsIPromptService);
+        var promptService = Services.prompt;
 
         var buttonPressed = promptService.confirmEx(window,
           this.mStrBundle.GetStringFromName("browsewithcaret.checkWindowTitle"),
           this.mStrBundle.GetStringFromName("browsewithcaret.checkLabel"),
           // Make "No" the default:
           promptService.STD_YES_NO_BUTTONS | promptService.BUTTON_POS_1_DEFAULT,
           null, null, null, this.mStrBundle.GetStringFromName("browsewithcaret.checkMsg"),
           checkValue);
@@ -62,48 +92,48 @@ class MozBrowser extends MozXULElement {
 
       // Toggle the pref
       try {
         this.mPrefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn);
       } catch (ex) {}
     }, { mozSystemGroup: true });
 
     this.addEventListener("dragover", (event) => {
-      if (!this.droppedLinkHandler || event.defaultPrevented)
+      if (!this.droppedLinkHandler || event.defaultPrevented) {
         return;
+      }
 
       // For drags that appear to be internal text (for example, tab drags),
       // set the dropEffect to 'none'. This prevents the drop even if some
       // other listener cancelled the event.
       var types = event.dataTransfer.types;
       if (types.includes("text/x-moz-text-internal") && !types.includes("text/plain")) {
         event.dataTransfer.dropEffect = "none";
         event.stopPropagation();
         event.preventDefault();
       }
 
       // No need to handle "dragover" in e10s, since nsDocShellTreeOwner.cpp in the child process
       // handles that case using "@mozilla.org/content/dropped-link-handler;1" service.
       if (this.isRemoteBrowser)
         return;
 
-      let linkHandler = Cc["@mozilla.org/content/dropped-link-handler;1"].
-      getService(Ci.nsIDroppedLinkHandler);
+      let linkHandler = Services.droppedLinkHandler;
       if (linkHandler.canDropLink(event, false))
         event.preventDefault();
     }, { mozSystemGroup: true });
 
     this.addEventListener("drop", (event) => {
       // No need to handle "drop" in e10s, since nsDocShellTreeOwner.cpp in the child process
       // handles that case using "@mozilla.org/content/dropped-link-handler;1" service.
-      if (!this.droppedLinkHandler || event.defaultPrevented || this.isRemoteBrowser)
+      if (!this.droppedLinkHandler || event.defaultPrevented || this.isRemoteBrowser) {
         return;
+      }
 
-      let linkHandler = Cc["@mozilla.org/content/dropped-link-handler;1"].
-      getService(Ci.nsIDroppedLinkHandler);
+      let linkHandler = Services.droppedLinkHandler;
       try {
         // Pass true to prevent the dropping of javascript:/data: URIs
         var links = linkHandler.dropLinks(event, true);
       } catch (ex) {
         return;
       }
 
       if (links.length) {
@@ -118,24 +148,49 @@ class MozBrowser extends MozXULElement {
       // with the mouse movement.
       if (this.isRemoteBrowser) {
         event.stopPropagation();
       }
     });
 
   }
 
-  connectedCallback() {
-    if (this.delayConnectedCallback()) {
-      return;
+  resetFields() {
+    if (this.observer) {
+      try {
+        Services.obs.removeObserver(this.observer, "browser:purge-session-history");
+      } catch (ex) {
+        // It's not clear why this sometimes throws an exception.
+      }
+      this.observer = null;
     }
-    this.textContent = "";
-    this.appendChild(MozXULElement.parseXULToFragment(`
-      <children></children>
-    `));
+
+    let browser = this;
+    this.observer = {
+      observe(aSubject, aTopic, aState) {
+        if (aTopic == "browser:purge-session-history") {
+          browser.purgeSessionHistory();
+        } else if (aTopic == "apz:cancel-autoscroll") {
+          if (aState == browser._autoScrollScrollId) {
+            // Set this._autoScrollScrollId to null, so in stopScroll() we
+            // don't call stopApzAutoscroll() (since it's APZ that
+            // initiated the stopping).
+            browser._autoScrollScrollId = null;
+            browser._autoScrollPresShellId = null;
+
+            browser._autoScrollPopup.hidePopup();
+          }
+        }
+      },
+      QueryInterface: ChromeUtils.generateQI([
+        Ci.nsIObserver,
+        Ci.nsISupportsWeakReference,
+      ]),
+    };
+
 
     this._documentURI = null;
 
     this._documentContentType = null;
 
     /**
      * Weak reference to an optional frame loader that can be used to influence
      * process selection for this browser.
@@ -182,17 +237,17 @@ class MozBrowser extends MozXULElement {
     this._contentRequestContextID = null;
 
     this._fullZoom = 1;
 
     this._textZoom = 1;
 
     this._isSyntheticDocument = false;
 
-    this.mPrefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+    this.mPrefs = Services.prefs;
 
     this._mStrBundle = null;
 
     this.blockedPopups = null;
 
     this._audioMuted = false;
 
     this._hasAnyPlayingMediaBeenBlocked = false;
@@ -218,27 +273,16 @@ class MozBrowser extends MozXULElement {
       },
       userTyped() {
         this._startedLoadSinceLastUserTyping = false;
       },
     };
 
     this._userTypedValue = null;
 
-    this.droppedLinkHandler = null;
-
-    this.mIconURL = null;
-
-    /**
-     * This is managed by the tabbrowser
-     */
-    this.lastURI = null;
-
-    this.mDestroyed = false;
-
     this._AUTOSCROLL_SNAP = 10;
 
     this._scrolling = false;
 
     this._startX = null;
 
     this._startY = null;
 
@@ -249,19 +293,31 @@ class MozBrowser extends MozXULElement {
     /**
      * These IDs identify the scroll frame being autoscrolled.
      */
     this._autoScrollScrollId = null;
 
     this._autoScrollPresShellId = null;
 
     this._permitUnloadId = 0;
+  }
+
+  connectedCallback() {
+    // We typically use this to avoid running JS that triggers a layout during parse
+    // (see comment on the delayConnectedCallback implementation). In this case, we
+    // are using it to avoid a leak - see https://bugzilla.mozilla.org/show_bug.cgi?id=1441935#c20.
+    if (this.delayConnectedCallback()) {
+      return;
+    }
 
     this.construct();
+  }
 
+  disconnectedCallback() {
+    this.destroy();
   }
 
   get autoscrollEnabled() {
     if (this.getAttribute("autoscroll") == "false")
       return false;
 
     return this.mPrefs.getBoolPref("general.autoScroll", true);
   }
@@ -307,21 +363,21 @@ class MozBrowser extends MozXULElement {
     let { frameLoader } = this;
     if (!frameLoader)
       return null;
     this._loadContext = frameLoader.loadContext;
     return this._loadContext;
   }
 
   get autoCompletePopup() {
-    return document.getElementById(this.getAttribute('autocompletepopup'))
+    return document.getElementById(this.getAttribute("autocompletepopup"));
   }
 
   get dateTimePicker() {
-    return document.getElementById(this.getAttribute('datetimepicker'))
+    return document.getElementById(this.getAttribute("datetimepicker"));
   }
 
   set docShellIsActive(val) {
     if (this.isRemoteBrowser) {
       this.frameLoader.tabParent.docShellIsActive = val;
       return val;
     }
     if (this.docShell)
@@ -380,17 +436,17 @@ class MozBrowser extends MozXULElement {
 
     try {
       return { width: document.imageRequest.image.width, height: document.imageRequest.image.height };
     } catch (e) {}
     return null;
   }
 
   get isRemoteBrowser() {
-    return (this.getAttribute('remote') == 'true');
+    return (this.getAttribute("remote") == "true");
   }
 
   get remoteType() {
     if (!this.isRemoteBrowser) {
       return null;
     }
 
     let remoteType = this.getAttribute("remoteType");
@@ -485,21 +541,21 @@ class MozBrowser extends MozXULElement {
     }
 
     return ChromeUtils.getBrowsingContext(this._browsingContextId);
   }
   /**
    * Note that this overrides webNavigation on XULFrameElement, and duplicates the return value for the non-remote case
    */
   get webNavigation() {
-    return this.isRemoteBrowser ? this._remoteWebNavigation : this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation);
+    return this.isRemoteBrowser ? this._remoteWebNavigation : this.docShell && this.docShell.QueryInterface(Ci.nsIWebNavigation);
   }
 
   get webProgress() {
-    return this.isRemoteBrowser ? this._remoteWebProgress : this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);
+    return this.isRemoteBrowser ? this._remoteWebProgress : this.docShell && this.docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
   }
 
   get sessionHistory() {
     return this.webNavigation.sessionHistory;
   }
 
   get markupDocumentViewer() {
     return this.docShell.contentViewer;
@@ -539,23 +595,25 @@ class MozBrowser extends MozXULElement {
       return this.contentDocument.documentLoadGroup
         .requestContextID;
     } catch (e) {
       return null;
     }
   }
 
   set showWindowResizer(val) {
-    if (val) this.setAttribute('showresizer', 'true');
-    else this.removeAttribute('showresizer');
-    return val;
+    if (val) {
+      this.setAttribute("showresizer", "true");
+    } else {
+      this.removeAttribute("showresizer");
+    }
   }
 
   get showWindowResizer() {
-    return this.getAttribute('showresizer') == 'true';
+    return this.getAttribute("showresizer") == "true";
   }
 
   set fullZoom(val) {
     if (this.isRemoteBrowser) {
       let changed = val.toFixed(2) != this._fullZoom.toFixed(2);
 
       if (changed) {
         this._fullZoom = val;
@@ -617,18 +675,17 @@ class MozBrowser extends MozXULElement {
     }
     return !!this.contentWindow.opener;
   }
 
   get mStrBundle() {
     if (!this._mStrBundle) {
       // need to create string bundle manually instead of using <xul:stringbundle/>
       // see bug 63370 for details
-      this._mStrBundle = Cc["@mozilla.org/intl/stringbundle;1"]
-        .getService(Ci.nsIStringBundleService)
+      this._mStrBundle = Services.strings
         .createBundle("chrome://global/locale/browser.properties");
     }
     return this._mStrBundle;
   }
 
   get audioMuted() {
     return this._audioMuted;
   }
@@ -945,16 +1002,19 @@ class MozBrowser extends MozXULElement {
   }
 
   didStartLoadSinceLastUserTyping() {
     return !this.inLoadURI &&
       this.urlbarChangeTracker._startedLoadSinceLastUserTyping;
   }
 
   construct() {
+    elementsToDestroyOnUnload.add(this);
+    this.resetFields();
+    this.mInitialized = true;
     if (this.isRemoteBrowser) {
       /*
        * Don't try to send messages from this function. The message manager for
        * the <browser> element may not be initialized yet.
        */
 
       this._remoteWebNavigation = Cc["@mozilla.org/remote-web-navigation;1"]
         .createInstance(Ci.nsIWebNavigation);
@@ -993,35 +1053,33 @@ class MozBrowser extends MozXULElement {
       this.messageManager.loadFrameScript("chrome://global/content/browser-child.js", true);
 
       if (this.hasAttribute("selectmenulist")) {
         this.messageManager.addMessageListener("Forms:ShowDropDown", this);
         this.messageManager.addMessageListener("Forms:HideDropDown", this);
       }
 
       if (!this.hasAttribute("disablehistory")) {
-        Services.obs.addObserver(this, "browser:purge-session-history", true);
+        Services.obs.addObserver(this.observer, "browser:purge-session-history", true);
       }
 
       let rc_js = "resource://gre/modules/RemoteController.js";
       let scope = {};
       Services.scriptloader.loadSubScript(rc_js, scope);
       let RemoteController = scope.RemoteController;
       this._controller = new RemoteController(this);
       this.controllers.appendController(this._controller);
     }
 
     try {
       // |webNavigation.sessionHistory| will have been set by the frame
       // loader when creating the docShell as long as this xul:browser
       // doesn't have the 'disablehistory' attribute set.
       if (this.docShell && this.webNavigation.sessionHistory) {
-        var os = Cc["@mozilla.org/observer-service;1"]
-          .getService(Ci.nsIObserverService);
-        os.addObserver(this, "browser:purge-session-history", true);
+        Services.obs.addObserver(this.observer, "browser:purge-session-history", true);
 
         // enable global history if we weren't told otherwise
         if (!this.hasAttribute("disableglobalhistory") && !this.isRemoteBrowser) {
           try {
             this.docShell.useGlobalHistory = true;
           } catch (ex) {
             // This can occur if the Places database is locked
             Cu.reportError("Error enabling browser global history: " + ex);
@@ -1074,58 +1132,41 @@ class MozBrowser extends MozXULElement {
     }
   }
 
   /**
    * This is necessary because the destructor doesn't always get called when
    * we are removed from a tabbrowser. This will be explicitly called by tabbrowser.
    */
   destroy() {
+    elementsToDestroyOnUnload.delete(this);
+
     // Make sure that any open select is closed.
     if (this._selectParentHelper) {
       let menulist = document.getElementById(this.getAttribute("selectmenulist"));
       this._selectParentHelper.hide(menulist, this);
     }
-    if (this.mDestroyed)
+
+    this.resetFields();
+
+    if (!this.mInitialized)
       return;
-    this.mDestroyed = true;
+
+    this.mInitialized = false;
 
     if (this.isRemoteBrowser) {
       try {
         this.controllers.removeController(this._controller);
       } catch (ex) {
         // This can fail when this browser element is not attached to a
         // BrowserDOMWindow.
       }
-
-      if (!this.hasAttribute("disablehistory")) {
-        let Services = ChromeUtils.import("resource://gre/modules/Services.jsm", {}).Services;
-        try {
-          Services.obs.removeObserver(this, "browser:purge-session-history");
-        } catch (ex) {
-          // It's not clear why this sometimes throws an exception.
-        }
-      }
-
       return;
     }
 
-    if (this.docShell && this.webNavigation.sessionHistory) {
-      var os = Cc["@mozilla.org/observer-service;1"]
-        .getService(Ci.nsIObserverService);
-      try {
-        os.removeObserver(this, "browser:purge-session-history");
-      } catch (ex) {
-        // It's not clear why this sometimes throws an exception.
-      }
-    }
-
-    this._fastFind = null;
-    this._webBrowserFind = null;
-
     this.lastURI = null;
 
     if (!this.isRemoteBrowser) {
       this.removeEventListener("pagehide", this.onPageHide, true);
     }
 
     if (this._autoScrollNeedsCleanup) {
       // we polluted the global scope, so clean it up
@@ -1159,19 +1200,18 @@ class MozBrowser extends MozXULElement {
           let usingApz = false;
           if (this.isRemoteBrowser && data.scrollId != null &&
             this.mPrefs.getBoolPref("apz.autoscroll.enabled", false)) {
             let { tabParent } = this.frameLoader;
             if (tabParent) {
               // If APZ is handling the autoscroll, it may decide to cancel
               // it of its own accord, so register an observer to allow it
               // to notify us of that.
-              var os = Cc["@mozilla.org/observer-service;1"]
-                .getService(Ci.nsIObserverService);
-              os.addObserver(this, "apz:cancel-autoscroll", true);
+              var os = Services.obs;
+              os.addObserver(this.observer, "apz:cancel-autoscroll", true);
 
               usingApz = tabParent.startApzAutoscroll(
                 data.screenX, data.screenY,
                 data.scrollId, data.presShellId);
             }
             // Save the IDs for later
             this._autoScrollScrollId = data.scrollId;
             this._autoScrollPresShellId = data.presShellId;
@@ -1220,17 +1260,16 @@ class MozBrowser extends MozXULElement {
       case "Forms:HideDropDown":
         {
           if (this._selectParentHelper) {
             let menulist = document.getElementById(this.getAttribute("selectmenulist"));
             this._selectParentHelper.hide(menulist, this);
           }
           break;
         }
-
     }
     return undefined;
   }
 
   receiveMessage(aMessage) {
     if (!this.isRemoteBrowser) {
       return this._receiveMessage(aMessage);
     }
@@ -1307,32 +1346,16 @@ class MozBrowser extends MozXULElement {
   enableDisableCommandsRemoteOnly(aAction, aEnabledLength, aEnabledCommands, aDisabledLength, aDisabledCommands) {
     if (this._controller) {
       this._controller.enableDisableCommands(aAction,
         aEnabledLength, aEnabledCommands,
         aDisabledLength, aDisabledCommands);
     }
   }
 
-  observe(aSubject, aTopic, aState) {
-    if (aTopic == "browser:purge-session-history") {
-      this.purgeSessionHistory();
-    } else if (aTopic == "apz:cancel-autoscroll") {
-      if (aState == this._autoScrollScrollId) {
-        // Set this._autoScrollScrollId to null, so in stopScroll() we
-        // don't call stopApzAutoscroll() (since it's APZ that
-        // initiated the stopping).
-        this._autoScrollScrollId = null;
-        this._autoScrollPresShellId = null;
-
-        this._autoScrollPopup.hidePopup();
-      }
-    }
-  }
-
   purgeSessionHistory() {
     if (this.isRemoteBrowser) {
       try {
         this.messageManager.sendAsyncMessage("Browser:PurgeSessionHistory");
       } catch (ex) {
         // This can throw if the browser has started to go away.
         if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
           throw ex;
@@ -1369,20 +1392,18 @@ class MozBrowser extends MozXULElement {
       window.removeEventListener("mouseup", this, true);
       window.removeEventListener("DOMMouseScroll", this, true);
       window.removeEventListener("contextmenu", this, true);
       window.removeEventListener("keydown", this, true);
       window.removeEventListener("keypress", this, true);
       window.removeEventListener("keyup", this, true);
       this.messageManager.sendAsyncMessage("Autoscroll:Stop");
 
-      var os = Cc["@mozilla.org/observer-service;1"]
-        .getService(Ci.nsIObserverService);
       try {
-        os.removeObserver(this, "apz:cancel-autoscroll");
+        Services.obs.removeObserver(this.observer, "apz:cancel-autoscroll");
       } catch (ex) {
         // It's not clear why this sometimes throws an exception
       }
 
       if (this.isRemoteBrowser && this._autoScrollScrollId != null) {
         let { tabParent } = this.frameLoader;
         if (tabParent) {
           tabParent.stopApzAutoscroll(this._autoScrollScrollId,
@@ -1764,17 +1785,17 @@ class MozBrowser extends MozXULElement {
       return { permitUnload, timedOut };
     }
 
     if (!this.docShell || !this.docShell.contentViewer) {
       return { permitUnload: true, timedOut: false };
     }
     return {
       permitUnload: this.docShell.contentViewer.permitUnload(aPermitUnloadFlags),
-      timedOut: false
+      timedOut: false,
     };
   }
 
   print(aOuterWindowID, aPrintSettings, aPrintProgressListener) {
     if (!this.frameLoader) {
       throw Components.Exception("No frame loader.",
         Cr.NS_ERROR_FAILURE);
     }
@@ -1810,17 +1831,14 @@ class MozBrowser extends MozXULElement {
   getContentBlockingLog() {
     if (this.isRemoteBrowser) {
       return this.frameLoader.tabParent.getContentBlockingLog();
     }
     return this.docShell ?
       this.docShell.getContentBlockingLog() :
       Promise.reject("docshell isn't available");
   }
-  disconnectedCallback() {
-    this.destroy();
-  }
 }
 
-MozXULElement.implementCustomInterface(MozBrowser, [Ci.nsIObserver, Ci.nsIBrowser]);
+MozXULElement.implementCustomInterface(MozBrowser, [Ci.nsIBrowser, Ci.nsIFrameLoaderOwner]);
 customElements.define("browser", MozBrowser);
 
 }
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -167,20 +167,16 @@ iframe {
 @supports -moz-bool-pref("layout.css.emulate-moz-box-with-flex") {
   browser,
   editor,
   iframe {
     display: block;
   }
 }
 
-browser {
-  -moz-binding: url("chrome://global/content/bindings/browser.xml#browser");
-}
-
 /*********** popup notification ************/
 popupnotification {
   -moz-binding: url("chrome://global/content/bindings/notification.xml#popup-notification");
 }
 
 .popup-notification-menubutton:not([label]) {
   display: none;
 }
--- a/toolkit/modules/E10SUtils.jsm
+++ b/toolkit/modules/E10SUtils.jsm
@@ -102,17 +102,17 @@ var E10SUtils = {
 
   getRemoteTypeForURI(aURL, aMultiProcess,
                       aPreferredRemoteType = DEFAULT_REMOTE_TYPE,
                       aCurrentUri) {
     if (!aMultiProcess) {
       return NOT_REMOTE;
     }
 
-    // loadURI in browser.xml treats null as about:blank
+    // loadURI in browser.js treats null as about:blank
     if (!aURL) {
       aURL = "about:blank";
     }
 
     let uri;
     try {
       uri = Services.uriFixup.createFixupURI(aURL, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
     } catch (e) {