Bug 1358815 - remove sync reflow from find bar initialization, r?jaws draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Mon, 12 Mar 2018 14:01:44 +0000
changeset 768052 e21e416d7c2a7a30b4091dc453f17f56525ef2bc
parent 767347 80b4777a6421d8df4bb27ac23fb607c318a3006c
child 768053 2b74c46e250956832b973d47102375179a15d9c6
child 768315 5d0a3265a9b7f8065f291278e707551d2539a194
push id102799
push usergijskruitbosch@gmail.com
push dateThu, 15 Mar 2018 17:55:18 +0000
reviewersjaws
bugs1358815, 1371523
milestone61.0a1
Bug 1358815 - remove sync reflow from find bar initialization, r?jaws This removes the sync reflow from almost all cases. The only case where we keep it is when a keypress caught in content triggers a sync message to the parent process. We should clean this up in bug 1371523. I've tried to fix the tests, but a lot of them seem to be disabled anyway... MozReview-Commit-ID: 9k36p7q8MKy
browser/base/content/browser-sets.inc
browser/base/content/browser.js
browser/base/content/tabbrowser.js
browser/base/content/test/general/browser_bug537013.js
browser/base/content/test/general/browser_bug567306.js
browser/base/content/test/general/browser_bug749738.js
browser/base/content/test/general/browser_findbarClose.js
browser/base/content/test/general/browser_zbug569342.js
browser/base/content/test/sanitize/browser_sanitize-formhistory.js
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/customizableui/test/browser_947914_button_find.js
browser/components/customizableui/test/browser_panel_keyboard_navigation.js
browser/components/uitour/test/browser_UITour_panel_close_annotation.js
browser/modules/Sanitizer.jsm
devtools/client/responsive.html/browser/swap.js
toolkit/content/tests/browser/browser_bug1198465.js
toolkit/content/tests/browser/browser_bug451286.js
toolkit/content/tests/browser/browser_bug982298.js
toolkit/content/tests/browser/browser_findbar.js
toolkit/content/tests/browser/browser_quickfind_editable.js
toolkit/content/widgets/findbar.xml
toolkit/modules/tests/browser/browser_FinderHighlighter.js
toolkit/modules/tests/browser/head.js
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -39,26 +39,26 @@
 
 #include ../../../toolkit/content/editMenuCommands.inc.xul
 
     <command id="View:PageSource" oncommand="BrowserViewSource(window.gBrowser.selectedBrowser);" observes="canViewSource"/>
     <command id="View:PageInfo" oncommand="BrowserPageInfo();"/>
     <command id="View:FullScreen" oncommand="BrowserFullScreen();"/>
     <command id="View:ReaderView" oncommand="ReaderParent.toggleReaderMode(event);"/>
     <command id="cmd_find"
-             oncommand="gFindBar.onFindCommand();"
+             oncommand="gLazyFindCommand('onFindCommand')"
              observes="isImage"/>
     <command id="cmd_findAgain"
-             oncommand="gFindBar.onFindAgainCommand(false);"
+             oncommand="gLazyFindCommand('onFindAgainCommand', false)"
              observes="isImage"/>
     <command id="cmd_findPrevious"
-             oncommand="gFindBar.onFindAgainCommand(true);"
+             oncommand="gLazyFindCommand('onFindAgainCommand', true)"
              observes="isImage"/>
 #ifdef XP_MACOSX
-    <command id="cmd_findSelection" oncommand="gFindBar.onFindSelectionCommand();"/>
+    <command id="cmd_findSelection" oncommand="gLazyFindCommand('onFindSelectionCommand')"/>
 #endif
     <!-- work-around bug 392512 -->
     <command id="Browser:AddBookmarkAs"
              oncommand="PlacesCommandHook.bookmarkPage(gBrowser.selectedBrowser, true);"/>
     <!-- The command disabled state must be manually updated through
          PlacesCommandHook.updateBookmarkAllTabsCommand() -->
     <command id="Browser:BookmarkAllTabs"
              oncommand="PlacesCommandHook.bookmarkCurrentPages();"/>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -261,28 +261,44 @@ if (AppConstants.platform != "macosx") {
 
 // Smart getter for the findbar.  If you don't wish to force the creation of
 // the findbar, check gFindBarInitialized first.
 
 Object.defineProperty(this, "gFindBar", {
   configurable: true,
   enumerable: true,
   get() {
-    return window.gBrowser.getFindBar();
+    return gBrowser.getCachedFindBar();
   },
 });
 
 Object.defineProperty(this, "gFindBarInitialized", {
   configurable: true,
   enumerable: true,
   get() {
-    return window.gBrowser.isFindBarInitialized();
+    return gBrowser.isFindBarInitialized();
   },
 });
 
+Object.defineProperty(this, "gFindBarPromise", {
+  configurable: true,
+  enumerable: true,
+  get() {
+    return gBrowser.getFindBar();
+  },
+});
+
+async function gLazyFindCommand(cmd, ...args) {
+  let fb = await gFindBarPromise;
+  // We could be closed by now, or the tab with XBL binding could have gone away:
+  if (fb && fb[cmd]) {
+    fb[cmd].apply(fb, args);
+  }
+}
+
 Object.defineProperty(this, "AddonManager", {
   configurable: true,
   enumerable: true,
   get() {
     let tmp = {};
     ChromeUtils.import("resource://gre/modules/AddonManager.jsm", tmp);
     return this.AddonManager = tmp.AddonManager;
   },
@@ -2055,17 +2071,17 @@ function HandleAppCommandEvent(evt) {
     break;
   case "New":
     BrowserOpenTab();
     break;
   case "Close":
     BrowserCloseTabOrWindow();
     break;
   case "Find":
-    gFindBar.onFindCommand();
+    gLazyFindCommand("onFindCommand");
     break;
   case "Help":
     openHelpLink("firefox-help");
     break;
   case "Open":
     BrowserOpenFileWindow();
     break;
   case "Print":
@@ -3503,17 +3519,17 @@ var PrintPreviewListener = {
       syncNotifications.notificationsHidden = true;
     }
   },
   _showChrome() {
     if (this._chromeState.notificationsOpen)
       gBrowser.getNotificationBox().notificationsHidden = false;
 
     if (this._chromeState.findOpen)
-      gFindBar.open();
+      gLazyFindCommand("open");
 
     if (this._chromeState.globalNotificationsOpen)
       document.getElementById("global-notificationbox").notificationsHidden = false;
 
     if (this._chromeState.syncNotificationsOpen)
       document.getElementById("sync-notifications").notificationsHidden = false;
 
     if (this._chromeState.sidebarOpen)
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -471,30 +471,64 @@ window._gBrowser = {
   get userTypedValue() {
     return this.mCurrentBrowser.userTypedValue;
   },
 
   isFindBarInitialized(aTab) {
     return (aTab || this.selectedTab)._findBar != undefined;
   },
 
-  getFindBar(aTab) {
-    if (!aTab)
-      aTab = this.selectedTab;
-
-    if (aTab._findBar)
-      return aTab._findBar;
-
+  /**
+   * Get the already constructed findbar
+   */
+  getCachedFindBar(aTab = this.selectedTab) {
+    return aTab._findBar;
+  },
+
+  /**
+   * Get the findbar, and create it if it doesn't exist.
+   * @return the find bar (or null if the window or tab is closed/closing in the interim).
+   */
+  async getFindBar(aTab = this.selectedTab) {
+    let findBar = this.getCachedFindBar(aTab);
+    if (findBar) {
+      return findBar;
+    }
+
+    // Avoid re-entrancy by caching the promise we're about to return.
+    if (!aTab._pendingFindBar) {
+      aTab._pendingFindBar = this._createFindBar(aTab);
+    }
+    return aTab._pendingFindBar;
+  },
+
+  /**
+   * Create a findbar instance.
+   * @param aTab the tab to create the find bar for.
+   * @param aForce Whether to force a sync flush to trigger XBL construction immediately.
+   * @return the created findbar, or null if the window or tab is closed/closing.
+   */
+  async _createFindBar(aTab, aForce = false) {
     let findBar = document.createElementNS(this._XUL_NS, "findbar");
     let browser = this.getBrowserForTab(aTab);
     let browserContainer = this.getBrowserContainer(browser);
     browserContainer.appendChild(findBar);
 
-    // Force a style flush to ensure that our binding is attached.
-    findBar.clientTop;
+    if (aForce) {
+      // Force a style flush to ensure that our binding is attached.
+      // Remove after bug 1371523 makes more of this async.
+      findBar.clientTop;
+    } else {
+      await new Promise(r => requestAnimationFrame(r));
+      if (window.closed || aTab.closing) {
+        delete aTab._pendingFindBar;
+        return null;
+      }
+    }
+    delete aTab._pendingFindBar;
 
     findBar.browser = browser;
     findBar._findField.value = this._lastFindValue;
 
     aTab._findBar = findBar;
 
     let event = document.createEvent("Events");
     event.initEvent("TabFindInitialized", true, false);
@@ -1118,17 +1152,17 @@ window._gBrowser = {
     }
 
     let oldBrowser = oldTab.linkedBrowser;
     let newBrowser = newTab.linkedBrowser;
 
     oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused);
 
     if (this.isFindBarInitialized(oldTab)) {
-      let findBar = this.getFindBar(oldTab);
+      let findBar = this.getCachedFindBar(oldTab);
       oldTab._findBarFocused = (!findBar.hidden &&
         findBar._findField.getAttribute("focused") == "true");
     }
 
     let activeEl = document.activeElement;
     // If focus is on the old tab, move it to the new tab.
     if (activeEl == oldTab) {
       newTab.focus();
@@ -1692,17 +1726,17 @@ window._gBrowser = {
       this._outerWindowIDBrowserMap.set(aBrowser.outerWindowID, aBrowser);
     }
 
     if (wasActive)
       aBrowser.focus();
 
     // If the findbar has been initialised, reset its browser reference.
     if (this.isFindBarInitialized(tab)) {
-      this.getFindBar(tab).browser = aBrowser;
+      this.getCachedFindBar(tab).browser = aBrowser;
     }
 
     evt = document.createEvent("Events");
     evt.initEvent("TabRemotenessChange", true, false);
     tab.dispatchEvent(evt);
 
     return true;
   },
@@ -3107,20 +3141,27 @@ window._gBrowser = {
         otherBrowser.getAttribute("usercontextid") || 0);
       delete otherBrowser.registeredOpenURI;
     }
 
     // Handle findbar data (if any)
     let otherFindBar = aOtherTab._findBar;
     if (otherFindBar &&
         otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
-      let ourFindBar = this.getFindBar(aOurTab);
-      ourFindBar._findField.value = otherFindBar._findField.value;
-      if (!otherFindBar.hidden)
-        ourFindBar.onFindCommand();
+      let oldValue = otherFindBar._findField.value;
+      let wasHidden = otherFindBar.hidden;
+      let ourFindBarPromise = this.getFindBar(aOurTab);
+      ourFindBarPromise.then(ourFindBar => {
+        if (!ourFindBar) {
+          return;
+        }
+        ourFindBar._findField.value = oldValue;
+        if (!wasHidden)
+          ourFindBar.onFindCommand();
+      });
     }
 
     // Finish tearing down the tab that's going away.
     if (closeWindow) {
       aOtherTab.ownerGlobal.close();
     } else {
       remoteBrowser._endRemoveTab(aOtherTab);
     }
@@ -3817,17 +3858,23 @@ window._gBrowser = {
             const FAYT_LINKS_KEY = "'";
             const FAYT_TEXT_KEY = "/";
             let charCode = data.fakeEvent.charCode;
             let key = charCode ? String.fromCharCode(charCode) : null;
             shouldFastFind = key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY;
           }
           if (shouldFastFind) {
             // Make sure we return the result.
-            return this.getFindBar(tab).receiveMessage(aMessage);
+            // This needs sync initialization of the find bar, unfortunately.
+            // bug 1371523 tracks removing all of this.
+
+            // This returns a promise, so don't use the result...
+            this._createFindBar(tab, true);
+            // ... just grab the 'cached' version now we know it exists.
+            this.getCachedFindBar().receiveMessage(aMessage);
           }
         }
         break;
       }
       case "RefreshBlocker:Blocked":
       {
         // The data object is expected to contain the following properties:
         //  - URI (string)
@@ -4515,17 +4562,17 @@ class TabProgressListener {
       }
 
       // If the browser was previously muted, we should restore the muted state.
       if (this.mTab.hasAttribute("muted")) {
         this.mTab.linkedBrowser.mute();
       }
 
       if (gBrowser.isFindBarInitialized(this.mTab)) {
-        let findBar = gBrowser.getFindBar(this.mTab);
+        let findBar = gBrowser.getCachedFindBar(this.mTab);
 
         // Close the Find toolbar if we're in old-style TAF mode
         if (findBar.findMode != findBar.FIND_NORMAL) {
           findBar.close();
         }
       }
 
       gBrowser.setTabTitle(this.mTab);
--- a/browser/base/content/test/general/browser_bug537013.js
+++ b/browser/base/content/test/general/browser_bug537013.js
@@ -37,29 +37,31 @@ function test() {
       gBrowser.removeTab(tabs.pop());
     }
   });
   texts.forEach(aText => addTabWithText(aText));
 
   // Set up the first tab
   gBrowser.selectedTab = tabs[0];
 
+  gBrowser.getFindBar().then(initialTest);
+}
+
+function initialTest() {
   setFindString(texts[0]);
   // Turn on highlight for testing bug 891638
   gFindBar.getElement("highlight").checked = true;
 
   // Make sure the second tab is correct, then set it up
   gBrowser.selectedTab = tabs[1];
-  gBrowser.selectedTab.addEventListener("TabFindInitialized", continueTests1);
+  gBrowser.selectedTab.addEventListener("TabFindInitialized", continueTests1, {once: true});
   // Initialize the findbar
-  gFindBar;
+  gBrowser.getFindBar();
 }
 function continueTests1() {
-  gBrowser.selectedTab.removeEventListener("TabFindInitialized",
-                                           continueTests1);
   ok(true, "'TabFindInitialized' event properly dispatched!");
   ok(gFindBar.hidden, "Second tab doesn't show find bar!");
   gFindBar.open();
   is(gFindBar._findField.value, texts[0],
      "Second tab kept old find value for new initialization!");
   setFindString(texts[1]);
 
   // Confirm the first tab is still correct, ensure re-hiding works as expected
@@ -92,16 +94,21 @@ function continueTests3() {
   is(gFindBar._findField.getAttribute("focused"), "true",
      "Open findbar refocused on tab change!");
   gURLBar.focus();
   gBrowser.selectedTab = tabs[0];
   ok(gFindBar.hidden, "First tab doesn't show find bar!");
 
   // Set up a third tab, no tests here
   gBrowser.selectedTab = tabs[2];
+  gBrowser.selectedTab.addEventListener("TabFindInitialized", continueTests4, {once: true});
+  gBrowser.getFindBar();
+}
+
+function continueTests4() {
   setFindString(texts[2]);
 
   // Now we jump to the second, then first, and then fourth
   gBrowser.selectedTab = tabs[1];
   // Test for bug 892384
   ok(!gFindBar._findField.hasAttribute("focused"),
      "Open findbar not refocused on tab change!");
   gBrowser.selectedTab = tabs[0];
--- a/browser/base/content/test/general/browser_bug567306.js
+++ b/browser/base/content/test/general/browser_bug567306.js
@@ -16,17 +16,17 @@ add_task(async function() {
       resolve();
     });
     selectedBrowser.loadURI("data:text/html,<h1 id='h1'>Select Me</h1>");
   });
 
   await SimpleTest.promiseFocus(newwindow);
 
   ok(!newwindow.gFindBarInitialized, "find bar is not yet initialized");
-  let findBar = newwindow.gFindBar;
+  let findBar = await newwindow.gFindBarPromise;
 
   await ContentTask.spawn(selectedBrowser, { }, async function() {
     let elt = content.document.getElementById("h1");
     let selection = content.getSelection();
     let range = content.document.createRange();
     range.setStart(elt, 0);
     range.setEnd(elt, 1);
     selection.removeAllRanges();
--- a/browser/base/content/test/general/browser_bug749738.js
+++ b/browser/base/content/test/general/browser_bug749738.js
@@ -1,29 +1,30 @@
 /* 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/. */
 
 "use strict";
 
 const DUMMY_PAGE = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
 
-function test() {
-  waitForExplicitFinish();
+/**
+ * This test checks that if you search for something on one tab, then close
+ * that tab and have the find bar open on the next tab you get switched to,
+ * closing the find bar in that tab works without exceptions.
+ */
+add_task(async function test_bug749738() {
+  // Open find bar on initial tab.
+  await gFindBarPromise;
 
-  let tab = BrowserTestUtils.addTab(gBrowser);
-  gBrowser.selectedTab = tab;
-
-  BrowserTestUtils.loadURI(tab.linkedBrowser, DUMMY_PAGE);
-  BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
+  await BrowserTestUtils.withNewTab(DUMMY_PAGE, async function() {
+    await gFindBarPromise;
     gFindBar.onFindCommand();
     EventUtils.sendString("Dummy");
-    gBrowser.removeTab(tab);
+  });
 
-    try {
-      gFindBar.close();
-      ok(true, "findbar.close should not throw an exception");
-    } catch (e) {
-      ok(false, "findbar.close threw exception: " + e);
-    }
-    finish();
-  });
-}
+  try {
+    gFindBar.close();
+    ok(true, "findbar.close should not throw an exception");
+  } catch (e) {
+    ok(false, "findbar.close threw exception: " + e);
+  }
+});
--- a/browser/base/content/test/general/browser_findbarClose.js
+++ b/browser/base/content/test/general/browser_findbarClose.js
@@ -11,16 +11,17 @@ add_task(async function findbar_test() {
 
   let promise = ContentTask.spawn(newTab.linkedBrowser, null, async function() {
     await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded", false);
   });
   newTab.linkedBrowser.loadURI("http://example.com/browser/" +
     "browser/base/content/test/general/test_bug628179.html");
   await promise;
 
+  await gFindBarPromise;
   gFindBar.open();
 
   await new ContentTask.spawn(newTab.linkedBrowser, null, async function() {
     let iframe = content.document.getElementById("iframe");
     let awaitLoad = ContentTaskUtils.waitForEvent(iframe, "load", false);
     iframe.src = "http://example.org/";
     await awaitLoad;
   });
--- a/browser/base/content/test/general/browser_zbug569342.js
+++ b/browser/base/content/test/general/browser_zbug569342.js
@@ -1,80 +1,61 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
-var gTab = null;
-
-function load(url, cb) {
-  gTab = BrowserTestUtils.addTab(gBrowser, url);
-  gBrowser.addEventListener("load", function listener(event) {
-    if (event.target.location != url)
-      return;
+add_task(async function findBarDisabledOnSomePages() {
+  ok(!gFindBar || gFindBar.hidden, "Find bar should not be visible by default");
 
-    gBrowser.removeEventListener("load", listener, true);
-    // Trigger onLocationChange by switching tabs.
-    gBrowser.selectedTab = gTab;
-    cb();
-  }, true);
-}
-
-function test() {
-  waitForExplicitFinish();
-
-  ok(gFindBar.hidden, "Find bar should not be visible by default");
-
+  let findbarOpenedPromise = BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "TabFindInitialized");
+  document.documentElement.focus();
   // Open the Find bar before we navigate to pages that shouldn't have it.
   EventUtils.synthesizeKey("f", { accelKey: true });
+  await findbarOpenedPromise;
   ok(!gFindBar.hidden, "Find bar should be visible");
 
-  nextTest();
-}
+  let urls = [
+    "about:config",
+    "about:addons",
+  ];
 
-var urls = [
-  "about:config",
-  "about:addons",
-];
+  for (let url of urls) {
+    await testFindDisabled(url);
+  }
 
-function nextTest() {
-  let url = urls.shift();
-  if (url) {
-    testFindDisabled(url, nextTest);
-  } else {
-    // Make sure the find bar is re-enabled after disabled page is closed.
-    testFindEnabled("about:blank", function() {
-      EventUtils.synthesizeKey("KEY_Escape");
-      ok(gFindBar.hidden, "Find bar should now be hidden");
-      finish();
-    });
-  }
-}
+  // Make sure the find bar is re-enabled after disabled page is closed.
+  await testFindEnabled("about:about");
+  gFindBar.close();
+  ok(gFindBar.hidden, "Find bar should now be hidden");
+});
 
-function testFindDisabled(url, cb) {
-  load(url, function() {
-    ok(gFindBar.hidden, "Find bar should not be visible");
-    EventUtils.synthesizeKey("/", {}, gTab.linkedBrowser.contentWindow);
-    ok(gFindBar.hidden, "Find bar should not be visible");
+function testFindDisabled(url) {
+  return BrowserTestUtils.withNewTab(url, async function(browser) {
+    let waitForFindBar = async () => {
+      await new Promise(r => requestAnimationFrame(r));
+      await new Promise(r => Services.tm.dispatchToMainThread(r));
+    };
+    ok(!gFindBar || gFindBar.hidden, "Find bar should not be visible at the start");
+    await BrowserTestUtils.synthesizeKey("/", {}, browser);
+    await waitForFindBar();
+    ok(!gFindBar || gFindBar.hidden, "Find bar should not be visible after fast find");
     EventUtils.synthesizeKey("f", {accelKey: true});
-    ok(gFindBar.hidden, "Find bar should not be visible");
+    await waitForFindBar();
+    ok(!gFindBar || gFindBar.hidden, "Find bar should not be visible after find command");
     ok(document.getElementById("cmd_find").getAttribute("disabled"),
        "Find command should be disabled");
-
-    gBrowser.removeTab(gTab);
-    cb();
   });
 }
 
-function testFindEnabled(url, cb) {
-  load(url, function() {
+async function testFindEnabled(url) {
+  return BrowserTestUtils.withNewTab(url, async function(browser) {
     ok(!document.getElementById("cmd_find").getAttribute("disabled"),
        "Find command should not be disabled");
 
     // Open Find bar and then close it.
+    let findbarOpenedPromise = BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "TabFindInitialized");
     EventUtils.synthesizeKey("f", { accelKey: true });
+    await findbarOpenedPromise;
     ok(!gFindBar.hidden, "Find bar should be visible again");
     EventUtils.synthesizeKey("KEY_Escape");
     ok(gFindBar.hidden, "Find bar should now be hidden");
-
-    gBrowser.removeTab(gTab);
-    cb();
   });
 }
--- a/browser/base/content/test/sanitize/browser_sanitize-formhistory.js
+++ b/browser/base/content/test/sanitize/browser_sanitize-formhistory.js
@@ -17,16 +17,17 @@ add_task(async function test() {
                              reject();
                            }
                          },
                        });
   });
 
   // Sanitize now so we can test the baseline point.
   await Sanitizer.sanitize(["formdata"]);
+  await gFindBarPromise;
   ok(!gFindBar.hasTransactions, "pre-test baseline for sanitizer");
 
   gFindBar.getElement("findbar-textbox").value = "m";
   ok(gFindBar.hasTransactions, "formdata can be cleared after input");
 
   await Sanitizer.sanitize(["formdata"]);
   is(gFindBar.getElement("findbar-textbox").value, "", "findBar textbox should be empty after sanitize");
   ok(!gFindBar.hasTransactions, "No transactions after sanitize");
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -176,18 +176,18 @@ const CustomizableWidgets = [
       win.saveBrowser(win.gBrowser.selectedBrowser);
     }
   }, {
     id: "find-button",
     shortcutId: "key_find",
     tooltiptext: "find-button.tooltiptext3",
     onCommand(aEvent) {
       let win = aEvent.target.ownerGlobal;
-      if (win.gFindBar) {
-        win.gFindBar.onFindCommand();
+      if (win.gLazyFindCommand) {
+        win.gLazyFindCommand("onFindCommand");
       }
     }
   }, {
     id: "open-file-button",
     shortcutId: "openFileKb",
     tooltiptext: "open-file-button.tooltiptext3",
     onCommand(aEvent) {
       let win = aEvent.target.ownerGlobal;
--- a/browser/components/customizableui/test/browser_947914_button_find.js
+++ b/browser/components/customizableui/test/browser_947914_button_find.js
@@ -12,15 +12,19 @@ add_task(async function() {
   await waitForOverflowButtonShown();
 
   await document.getElementById("nav-bar").overflowable.show();
   info("Menu panel was opened");
 
   let findButton = document.getElementById("find-button");
   ok(findButton, "Find button exists in Panel Menu");
 
+  let findBarPromise = gBrowser.isFindBarInitialized() ?
+    null : BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "TabFindInitialized");
+
   findButton.click();
+  await findBarPromise;
   ok(!gFindBar.hasAttribute("hidden"), "Findbar opened successfully");
 
   // close find bar
   gFindBar.close();
   info("Findbar was closed");
 });
--- a/browser/components/customizableui/test/browser_panel_keyboard_navigation.js
+++ b/browser/components/customizableui/test/browser_panel_keyboard_navigation.js
@@ -89,22 +89,25 @@ add_task(async function testEnterKeyBeha
 
   // Let's test a 'normal' command button.
   focusedElement = document.commandDispatcher.focusedElement;
   const kFindButtonId = "appMenu-find-button";
   while (!focusedElement || !focusedElement.id || focusedElement.id != kFindButtonId) {
     EventUtils.synthesizeKey("KEY_ArrowUp");
     focusedElement = document.commandDispatcher.focusedElement;
   }
+  let findBarPromise = gBrowser.isFindBarInitialized() ?
+    null : BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "TabFindInitialized");
   Assert.equal(focusedElement.id, kFindButtonId, "Find button should be selected");
 
   promise = promisePanelHidden(window);
   EventUtils.synthesizeKey("KEY_Enter");
   await promise;
 
+  await findBarPromise;
   Assert.ok(!gFindBar.hidden, "Findbar should have opened");
   gFindBar.close();
 });
 
 add_task(async function testLeftRightKeys() {
   let promise = promisePanelShown(window);
   PanelUI.show();
   await promise;
--- a/browser/components/uitour/test/browser_UITour_panel_close_annotation.js
+++ b/browser/components/uitour/test/browser_UITour_panel_close_annotation.js
@@ -11,17 +11,20 @@ var gTestTab;
 var gContentAPI;
 var gContentWindow;
 var highlight = document.getElementById("UITourHighlight");
 var tooltip = document.getElementById("UITourTooltip");
 
 function test() {
   registerCleanupFunction(() => {
     // Close the find bar in case it's open in the remaining tab
-    gBrowser.getFindBar(gBrowser.selectedTab).close();
+    let findBar = gBrowser.getCachedFindBar(gBrowser.selectedTab);
+    if (findBar) {
+      findBar.close();
+    }
   });
   UITourTest();
 }
 
 var tests = [
   function test_highlight_move_outside_panel(done) {
     gContentAPI.showInfo("urlbar", "test title", "test text");
     gContentAPI.showHighlight("customize");
--- a/browser/modules/Sanitizer.jsm
+++ b/browser/modules/Sanitizer.jsm
@@ -489,17 +489,17 @@ var Sanitizer = {
               // No tab browser? This means that it's too early during startup (typically,
               // Session Restore hasn't completed yet). Since we don't have find
               // bars at that stage and since Session Restore will not restore
               // find bars further down during startup, we have nothing to clear.
               continue;
             }
             for (let tab of tabBrowser.tabs) {
               if (tabBrowser.isFindBarInitialized(tab))
-                tabBrowser.getFindBar(tab).clear();
+                tabBrowser.getCachedFindBar(tab).clear();
             }
             // Clear any saved find value
             tabBrowser._lastFindValue = "";
           }
         } catch (ex) {
           seenException = ex;
         }
 
--- a/devtools/client/responsive.html/browser/swap.js
+++ b/devtools/client/responsive.html/browser/swap.js
@@ -183,17 +183,17 @@ function swapToInnerBrowser({ tab, conta
       //    the content in the viewport, instead of the tool page.
       tunnel = tunnelToInnerBrowser(tab.linkedBrowser, innerBrowser);
       debug("Wait until tunnel start");
       await tunnel.start();
 
       // Swapping browsers disconnects the find bar UI from the browser.
       // If the find bar has been initialized, reconnect it.
       if (gBrowser.isFindBarInitialized(tab)) {
-        let findBar = gBrowser.getFindBar(tab);
+        let findBar = gBrowser.getCachedFindBar(tab);
         findBar.browser = tab.linkedBrowser;
         if (!findBar.hidden) {
           // Force the find bar to activate again, restoring the search string.
           findBar.onFindCommand();
         }
       }
 
       // Force the browser UI to match the new state of the tab and browser.
@@ -248,17 +248,17 @@ function swapToInnerBrowser({ tab, conta
       //    temporary tab used to hold the content via
       //    `swapBrowsersAndCloseOther`.
       dispatchDevToolsBrowserSwap(contentBrowser, tab.linkedBrowser);
       swapBrowsersAndCloseOtherSilently(tab, contentTab);
 
       // Swapping browsers disconnects the find bar UI from the browser.
       // If the find bar has been initialized, reconnect it.
       if (gBrowser.isFindBarInitialized(tab)) {
-        let findBar = gBrowser.getFindBar(tab);
+        let findBar = gBrowser.getCachedFindBar(tab);
         findBar.browser = tab.linkedBrowser;
         if (!findBar.hidden) {
           // Force the find bar to activate again, restoring the search string.
           findBar.onFindCommand();
         }
       }
 
       gBrowser = null;
--- a/toolkit/content/tests/browser/browser_bug1198465.js
+++ b/toolkit/content/tests/browser/browser_bug1198465.js
@@ -21,16 +21,21 @@ add_task(async function() {
   // So `yield BrowserTestUtils.sendChar()` can't be used here:
   //  - synthesizing a key in the browser won't actually send it to the
   //    findbar; the findbar isn't part of the browser content.
   //  - we need to _not_ wait for _startFindDeferred to be resolved; yielding
   //    a synthesized keypress on the browser implicitely happens after the
   //    browser has dispatched its return message with the prefill value for
   //    the findbar, which essentially nulls these tests.
 
+  // The parent-side of the sidebar initialization is also async, so we do
+  // need to wait for that. We verify a bit further down that _startFindDeferred
+  // hasn't been resolved yet.
+  await gFindBarPromise;
+
   let findBar = gFindBar;
   is(findBar._findField.value, "", "findbar is empty");
 
   // Test 1
   //  Any input in the findbar should erase a previous search.
 
   findBar._findField.value = "xy";
   findBar.startFind();
--- a/toolkit/content/tests/browser/browser_bug451286.js
+++ b/toolkit/content/tests/browser/browser_bug451286.js
@@ -130,16 +130,17 @@ function findAgainAndWait() {
       onMatchesCountResult() {}
     };
     gFindBar.browser.finder.addResultListener(listener);
     gFindBar.onFindAgainCommand();
   });
 }
 
 async function openFindBarAndWait() {
+  await gFindBarPromise;
   let awaitTransitionEnd = BrowserTestUtils.waitForEvent(gFindBar, "transitionend");
   gFindBar.open();
   await awaitTransitionEnd;
 }
 
 // This test is comparing snapshots. It is necessary to wait for the gFindBar
 // to close before taking the snapshot so the gFindBar does not take up space
 // on the new snapshot.
--- a/toolkit/content/tests/browser/browser_bug982298.js
+++ b/toolkit/content/tests/browser/browser_bug982298.js
@@ -15,16 +15,17 @@ add_task(async function() {
           resolve();
         },
         onCurrentSelection() {},
         onMatchesCountResult() {}
       };
       info("about to add results listener, open find bar, and send 'F' string");
       browser.finder.addResultListener(listener);
     });
+    await gFindBarPromise;
     gFindBar.onFindCommand();
     EventUtils.sendString("F");
     info("added result listener and sent string 'F'");
     await awaitFindResult;
 
     let awaitScrollDone = BrowserTestUtils.waitForMessage(browser.messageManager, "ScrollDone");
     // scroll textarea to bottom
     const scrollTest =
--- a/toolkit/content/tests/browser/browser_findbar.js
+++ b/toolkit/content/tests/browser/browser_findbar.js
@@ -12,17 +12,17 @@ const E10S_PARENT_TEST_PAGE_URI = "javas
  * by calling stopPropagation on a keypress event on a page.
  */
 add_task(async function test_hotkey_event_propagation() {
   info("Ensure hotkeys are not affected by stopPropagation.");
 
   // Opening new tab
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
   let browser = gBrowser.getBrowserForTab(tab);
-  let findbar = gBrowser.getFindBar();
+  let findbar = await gBrowser.getFindBar();
 
   // Pressing these keys open the findbar.
   const HOTKEYS = ["/", "'"];
 
   // Checking if findbar appears when any hotkey is pressed.
   for (let key of HOTKEYS) {
     is(findbar.hidden, true, "Findbar is hidden now.");
     gBrowser.selectedTab = tab;
@@ -59,42 +59,42 @@ add_task(async function test_hotkey_even
 
 add_task(async function test_not_found() {
   info("Check correct 'Phrase not found' on new tab");
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
 
   // Search for the first word.
   await promiseFindFinished("--- THIS SHOULD NEVER MATCH ---", false);
-  let findbar = gBrowser.getFindBar();
+  let findbar = gBrowser.getCachedFindBar();
   is(findbar._findStatusDesc.textContent, findbar._notFoundStr,
      "Findbar status text should be 'Phrase not found'");
 
   gBrowser.removeTab(tab);
 });
 
 add_task(async function test_found() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
 
   // Search for a string that WILL be found, with 'Highlight All' on
   await promiseFindFinished("S", true);
-  ok(!gBrowser.getFindBar()._findStatusDesc.textContent,
+  ok(!gBrowser.getCachedFindBar()._findStatusDesc.textContent,
      "Findbar status should be empty");
 
   gBrowser.removeTab(tab);
 });
 
 // Setting first findbar to case-sensitive mode should not affect
 // new tab find bar.
 add_task(async function test_tabwise_case_sensitive() {
   let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
-  let findbar1 = gBrowser.getFindBar();
+  let findbar1 = await gBrowser.getFindBar();
 
   let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
-  let findbar2 = gBrowser.getFindBar();
+  let findbar2 = await gBrowser.getFindBar();
 
   // Toggle case sensitivity for first findbar
   findbar1.getElement("find-case-sensitive").click();
 
   gBrowser.selectedTab = tab1;
 
   // Not found for first tab.
   await promiseFindFinished("S", true);
@@ -125,17 +125,17 @@ add_task(async function test_reinitializ
     return;
   }
 
   info("Ensure findbar re-initialization at remoteness change.");
 
   // Load a remote page and trigger findbar construction.
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
   let browser = gBrowser.getBrowserForTab(tab);
-  let findbar = gBrowser.getFindBar();
+  let findbar = await gBrowser.getFindBar();
 
   // Findbar should operate normally.
   await promiseFindFinished("z", false);
   is(findbar._findStatusDesc.textContent, findbar._notFoundStr,
      "Findbar status text should be 'Phrase not found'");
 
   await promiseFindFinished("s", false);
   ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty");
@@ -168,16 +168,17 @@ add_task(async function() {
     return;
   }
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
   let browser = tab.linkedBrowser;
 
   ok(!gFindBarInitialized, "findbar isn't initialized yet");
 
+  await gFindBarPromise;
   let findBar = gFindBar;
   let initialValue = findBar._findField.value;
 
   await EventUtils.synthesizeAndWaitKey("f", { accelKey: true }, window, null,
                                         () => {
     isnot(document.activeElement, findBar._findField.inputField,
       "findbar is not yet focused");
   });
@@ -194,19 +195,19 @@ add_task(async function() {
   is(document.activeElement, findBar._findField.inputField,
     "findbar is now focused");
   is(findBar._findField.value, "abc", "abc fully entered as find query");
 
   await BrowserTestUtils.removeTab(tab);
 });
 
 function promiseFindFinished(searchText, highlightOn) {
-  return new Promise(resolve => {
+  return new Promise(async (resolve) => {
 
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
     findbar.startFind(findbar.FIND_NORMAL);
     let highlightElement = findbar.getElement("highlight");
     if (highlightElement.checked != highlightOn)
       highlightElement.click();
     executeSoon(() => {
       findbar._findField.value = searchText;
 
       let resultListener;
--- a/toolkit/content/tests/browser/browser_quickfind_editable.js
+++ b/toolkit/content/tests/browser/browser_quickfind_editable.js
@@ -1,14 +1,14 @@
 const PAGE = "data:text/html,<div contenteditable>foo</div><input><textarea></textarea>";
 const DESIGNMODE_PAGE = "data:text/html,<body onload='document.designMode=\"on\";'>";
 const HOTKEYS = ["/", "'"];
 
 async function test_hotkeys(browser, expected) {
-  let findbar = gBrowser.getFindBar();
+  let findbar = await gBrowser.getFindBar();
   for (let key of HOTKEYS) {
     is(findbar.hidden, true, "findbar is hidden");
     await BrowserTestUtils.sendChar(key, gBrowser.selectedBrowser);
     is(findbar.hidden, expected, "findbar should" + (expected ? "" : " not") + " be hidden");
     if (!expected) {
       await closeFindbarAndWait(findbar);
     }
   }
--- a/toolkit/content/widgets/findbar.xml
+++ b/toolkit/content/widgets/findbar.xml
@@ -415,17 +415,17 @@
            we are removed from a document that is not destroyed. This
            needs to be explicitly called in this case -->
       <method name="destroy">
         <body><![CDATA[
           if (this._destroyed)
             return;
           this._destroyed = true;
 
-          if (this.browser.finder)
+          if (this.browser && this.browser.finder)
             this.browser.finder.destroy();
 
           this.browser = null;
 
           let prefsvc = this._prefsvc;
           prefsvc.removeObserver("accessibility.typeaheadfind",
                                  this._observer);
           prefsvc.removeObserver("accessibility.typeaheadfind.linksonly",
--- a/toolkit/modules/tests/browser/browser_FinderHighlighter.js
+++ b/toolkit/modules/tests/browser/browser_FinderHighlighter.js
@@ -44,17 +44,17 @@ add_task(async function testModalResults
     ["o", {
       rectCount: 492,
       insertCalls: [1, 4],
       removeCalls: [0, 2]
     }]
   ]);
   let url = kFixtureBaseURL + "file_FinderSample.html";
   await BrowserTestUtils.withNewTab(url, async function(browser) {
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
 
     for (let [word, expectedResult] of tests) {
       await promiseOpenFindbar(findbar);
       Assert.ok(!findbar.hidden, "Findbar should be open now.");
 
       let timeout = kIteratorTimeout;
       if (word.length == 1)
         timeout *= 4;
@@ -71,17 +71,17 @@ add_task(async function testModalResults
   });
 });
 
 // Test if runtime switching of highlight modes between modal and non-modal works
 // as expected.
 add_task(async function testModalSwitching() {
   let url = kFixtureBaseURL + "file_FinderSample.html";
   await BrowserTestUtils.withNewTab(url, async function(browser) {
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
 
     await promiseOpenFindbar(findbar);
     Assert.ok(!findbar.hidden, "Findbar should be open now.");
 
     let word = "Roland";
     let expectedResult = {
       rectCount: 2,
       insertCalls: [2, 4],
@@ -108,17 +108,17 @@ add_task(async function testModalSwitchi
 
   await SpecialPowers.pushPrefEnv({ "set": [[ kPrefModalHighlight, true ]] });
 });
 
 // Test if highlighting a dark page is detected properly.
 add_task(async function testDarkPageDetection() {
   let url = kFixtureBaseURL + "file_FinderSample.html";
   await BrowserTestUtils.withNewTab(url, async function(browser) {
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
 
     await promiseOpenFindbar(findbar);
 
     let word = "Roland";
     let expectedResult = {
       rectCount: 2,
       insertCalls: [1, 3],
       removeCalls: [0, 1]
@@ -129,17 +129,17 @@ add_task(async function testDarkPageDete
     });
     await promiseEnterStringIntoFindField(findbar, word);
     await promise;
 
     findbar.close(true);
   });
 
   await BrowserTestUtils.withNewTab(url, async function(browser) {
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
 
     await promiseOpenFindbar(findbar);
 
     let word = "Roland";
     let expectedResult = {
       rectCount: 2,
       insertCalls: [2, 4],
       removeCalls: [0, 1]
@@ -167,17 +167,17 @@ add_task(async function testDarkPageDete
 
     findbar.close(true);
   });
 });
 
 add_task(async function testHighlightAllToggle() {
   let url = kFixtureBaseURL + "file_FinderSample.html";
   await BrowserTestUtils.withNewTab(url, async function(browser) {
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
 
     await promiseOpenFindbar(findbar);
 
     let word = "Roland";
     let expectedResult = {
       rectCount: 2,
       insertCalls: [2, 4],
       removeCalls: [0, 1]
@@ -211,17 +211,17 @@ add_task(async function testHighlightAll
 
 add_task(async function testXMLDocument() {
   let url = "data:text/xml;charset=utf-8," + encodeURIComponent(`<?xml version="1.0"?>
 <result>
   <Title>Example</Title>
   <Error>Error</Error>
 </result>`);
   await BrowserTestUtils.withNewTab(url, async function(browser) {
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
 
     await promiseOpenFindbar(findbar);
 
     let word = "Example";
     let expectedResult = {
       rectCount: 0,
       insertCalls: [1, 4],
       removeCalls: [0, 1]
@@ -233,17 +233,17 @@ add_task(async function testXMLDocument(
     findbar.close(true);
   });
 });
 
 add_task(async function testHideOnLocationChange() {
   let url = kFixtureBaseURL + "file_FinderSample.html";
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
   let browser = tab.linkedBrowser;
-  let findbar = gBrowser.getFindBar();
+  let findbar = await gBrowser.getFindBar();
 
   await promiseOpenFindbar(findbar);
 
   let word = "Roland";
   let expectedResult = {
     rectCount: 2,
     insertCalls: [2, 4],
     removeCalls: [0, 1]
@@ -262,17 +262,17 @@ add_task(async function testHideOnLocati
   await promise;
 
   await BrowserTestUtils.removeTab(tab);
 });
 
 add_task(async function testHideOnClear() {
   let url = kFixtureBaseURL + "file_FinderSample.html";
   await BrowserTestUtils.withNewTab(url, async function(browser) {
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
     await promiseOpenFindbar(findbar);
 
     let word = "Roland";
     let expectedResult = {
       rectCount: 2,
       insertCalls: [2, 4],
       removeCalls: [0, 2]
     };
@@ -294,17 +294,17 @@ add_task(async function testHideOnClear(
 });
 
 add_task(async function testRectsAndTexts() {
   let url = "data:text/html;charset=utf-8," +
     encodeURIComponent("<div style=\"width: 150px; border: 1px solid black\">" +
     "Here are a lot of words Please use find to highlight some words that wrap" +
     " across a line boundary and see what happens.</div>");
   await BrowserTestUtils.withNewTab(url, async function(browser) {
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
     await promiseOpenFindbar(findbar);
 
     let word = "words please use find to";
     let expectedResult = {
       rectCount: 2,
       insertCalls: [2, 4],
       removeCalls: [0, 2]
     };
@@ -317,17 +317,17 @@ add_task(async function testRectsAndText
     await promiseEnterStringIntoFindField(findbar, word);
     await promise;
   });
 });
 
 add_task(async function testTooLargeToggle() {
   let url = kFixtureBaseURL + "file_FinderSample.html";
   await BrowserTestUtils.withNewTab(url, async function(browser) {
-    let findbar = gBrowser.getFindBar();
+    let findbar = await gBrowser.getFindBar();
     await promiseOpenFindbar(findbar);
 
     await ContentTask.spawn(browser, null, async function() {
       let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIDOMWindowUtils);
       let uri = "data:text/css;charset=utf-8," + encodeURIComponent(`
         body {
           min-height: 1234567px;
--- a/toolkit/modules/tests/browser/head.js
+++ b/toolkit/modules/tests/browser/head.js
@@ -20,17 +20,18 @@ function removeDupes(list) {
 function compareLists(list1, list2, kind) {
   list1.sort();
   removeDupes(list1);
   list2.sort();
   removeDupes(list2);
   is(String(list1), String(list2), `${kind} URLs correct`);
 }
 
-function promiseOpenFindbar(findbar) {
+async function promiseOpenFindbar(findbar) {
+  await gBrowser.getFindBar();
   findbar.onFindCommand();
   return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise;
 }
 
 function promiseFindResult(findbar, str = null) {
   let highlightFinished = false;
   let findFinished = false;
   return new Promise(resolve => {