Repository: GoogleChrome/lighthouse (js)

Issue:
Performance Avoid an excessive DOM size error
<!-- Before creating an issue please make sure you are using the latest version and have checked for duplicate issues. -->

<!-- Before creating an Accessibility issue please test that it is reproducible upstream with axe (https://www.deque.com/axe/) first and file the issue there if necessary. -->

#### Provide the steps to reproduce
1. Run LH on https://www.americanexpress.com
  
<!-- If your page is only local, or is liable to change, consider uploading a repro so that we can more easily debug the problem. Some services that will help are: https://jsbin.com/, https://surge.sh/ -->

#### What is the current behavior?
Seeing this in Diagnostics:

Avoid an excessive DOM size 1,436 elements

Total DOM Elements  1436
Maximum DOM Depth 21
Maximum Child Elements 43

#### What is the expected behavior?
Since all these three values are less than the maximum(1500, 32 and 60 respectively), we should not be seeing this error. Are we missing something here?

#### Environment Information
* Affected Channels: <!-- CLI, Node, Extension, DevTools -->
* Lighthouse version:
* Node.js version:
* Operating System:

**Related issues**


Pull request:
misc: rewrite dom-size description
just following the path we decided immediately after we [found a super beautiful color to paint that shed](https://github.com/GoogleChrome/lighthouse/pull/9690). 😢

fixes #9725 
closes #9690

Patch:
diff --git a/.travis.yml b/.travis.yml
index 69ce7695c..be7f2b718 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -38,6 +38,7 @@ script:
   - yarn test-clients
   - yarn test-viewer
   - yarn test-lantern
+  - yarn test-bundle
   - yarn i18n:checks
   - yarn dogfood-lhci
 before_cache:
diff --git a/lighthouse-core/audits/critical-request-chains.js b/lighthouse-core/audits/critical-request-chains.js
index ebb7ba2e2..02d8ea384 100644
--- a/lighthouse-core/audits/critical-request-chains.js
+++ b/lighthouse-core/audits/critical-request-chains.js
@@ -11,7 +11,7 @@ const ComputedChains = require('../computed/critical-request-chains.js');
 
 const UIStrings = {
   /** Imperative title of a Lighthouse audit that tells the user to reduce the depth of critical network requests to enhance initial load of a page. Critical request chains are series of dependent network requests that are important for page rendering. For example, here's a 4-request-deep chain: The biglogo.jpg image is required, but is requested via the styles.css style code, which is requested by the initialize.js javascript, which is requested by the page's HTML. This is displayed in a list of audit titles that Lighthouse generates. */
-  title: 'Minimize Critical Requests Depth',
+  title: 'Avoid chaining critical requests',
   /** Description of a Lighthouse audit that tells the user *why* they should reduce the depth of critical network requests to enhance initial load of a page . This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
   description: 'The Critical Request Chains below show you what resources are ' +
       'loaded with a high priority. Consider reducing ' +
diff --git a/lighthouse-core/audits/dobetterweb/dom-size.js b/lighthouse-core/audits/dobetterweb/dom-size.js
index b0cfc7c09..c01436bb9 100644
--- a/lighthouse-core/audits/dobetterweb/dom-size.js
+++ b/lighthouse-core/audits/dobetterweb/dom-size.js
@@ -16,20 +16,13 @@ const Audit = require('../audit.js');
 const Util = require('../../report/html/renderer/util.js');
 const i18n = require('../../lib/i18n/i18n.js');
 
-const MAX_DOM_ELEMENTS = 1500;
-const MAX_DOM_TREE_WIDTH = 60;
-const MAX_DOM_TREE_DEPTH = 32;
-
 const UIStrings = {
   /** Title of a diagnostic audit that provides detail on the size of the web page's DOM. The size of a DOM is characterized by the total number of DOM elements and greatest DOM depth. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
   title: 'Avoids an excessive DOM size',
   /** Title of a diagnostic audit that provides detail on the size of the web page's DOM. The size of a DOM is characterized by the total number of DOM elements and greatest DOM depth. This imperative title is shown to users when there is a significant amount of execution time that could be reduced. */
   failureTitle: 'Avoid an excessive DOM size',
   /** Description of a Lighthouse audit that tells the user *why* they should reduce the size of the web page's DOM. The size of a DOM is characterized by the total number of DOM elements and greatest DOM depth. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
-  description: 'Browser engineers recommend pages contain fewer than ' +
-    `~${MAX_DOM_ELEMENTS.toLocaleString()} DOM elements. The sweet spot is a tree ` +
-    `depth < ${MAX_DOM_TREE_DEPTH} elements and fewer than ${MAX_DOM_TREE_WIDTH} ` +
-    'children/parent element. A large DOM can increase memory usage, cause longer ' +
+  description: 'A large DOM will increase memory usage, cause longer ' +
     '[style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), ' +
     'and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://web.dev/dom-size).',
   /** Table column header for the type of statistic. These statistics describe how big the DOM is (count of DOM elements, children, depth). */
@@ -55,10 +48,6 @@ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
 
 
 class DOMSize extends Audit {
-  static get MAX_DOM_ELEMENTS() {
-    return MAX_DOM_ELEMENTS;
-  }
-
   /**
    * @return {LH.Audit.Meta}
    */
diff --git a/lighthouse-core/audits/third-party-summary.js b/lighthouse-core/audits/third-party-summary.js
index 53e7a4f3c..23793edda 100644
--- a/lighthouse-core/audits/third-party-summary.js
+++ b/lighthouse-core/audits/third-party-summary.js
@@ -15,7 +15,7 @@ const MainThreadTasks = require('../computed/main-thread-tasks.js');
 
 const UIStrings = {
   /** Title of a diagnostic audit that provides details about the code on a web page that the user doesn't control (referred to as "third-party code"). This descriptive title is shown to users when the amount is acceptable and no user action is required. */
-  title: 'Third-Party usage',
+  title: 'Minimize third-party usage',
   /** Title of a diagnostic audit that provides details about the code on a web page that the user doesn't control (referred to as "third-party code"). This imperative title is shown to users when there is a significant amount of page execution time caused by third-party code that should be reduced. */
   failureTitle: 'Reduce the impact of third-party code',
   /** Description of a Lighthouse audit that identifies the code on the page that the user doesn't control. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js
index 77b7e566d..f4923e7f5 100644
--- a/lighthouse-core/gather/gatherers/image-elements.js
+++ b/lighthouse-core/gather/gatherers/image-elements.js
@@ -130,6 +130,12 @@ function determineNaturalSize(url) {
 }
 
 class ImageElements extends Gatherer {
+  constructor() {
+    super();
+    /** @type {Map<string, {naturalWidth: number, naturalHeight: number}>} */
+    this._naturalSizeCache = new Map();
+  }
+
   /**
    * @param {Driver} driver
    * @param {LH.Artifacts.ImageElement} element
@@ -137,11 +143,16 @@ class ImageElements extends Gatherer {
    */
   async fetchElementWithSizeInformation(driver, element) {
     const url = JSON.stringify(element.src);
+    if (this._naturalSizeCache.has(url)) {
+      return Object.assign(element, this._naturalSizeCache.get(url));
+    }
+
     try {
       // We don't want this to take forever, 250ms should be enough for images that are cached
       driver.setNextProtocolTimeout(250);
       /** @type {{naturalWidth: number, naturalHeight: number}} */
       const size = await driver.evaluateAsync(`(${determineNaturalSize.toString()})(${url})`);
+      this._naturalSizeCache.set(url, size);
       return Object.assign(element, size);
     } catch (_) {
       // determineNaturalSize fails on invalid images, which we treat as non-visible
diff --git a/lighthouse-core/lib/asset-saver.js b/lighthouse-core/lib/asset-saver.js
index 5490bcfae..677e61156 100644
--- a/lighthouse-core/lib/asset-saver.js
+++ b/lighthouse-core/lib/asset-saver.js
@@ -91,7 +91,7 @@ function stringifyReplacer(key, value) {
 }
 
 /**
- * Save artifacts object mostly to single file located at basePath/artifacts.log.
+ * Save artifacts object mostly to single file located at basePath/artifacts.json.
  * Also save the traces & devtoolsLogs to their own files
  * @param {LH.Artifacts} artifacts
  * @param {string} basePath
diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json
index 0d21d4a2e..d0e4f89c9 100644
--- a/lighthouse-core/lib/i18n/locales/en-US.json
+++ b/lighthouse-core/lib/i18n/locales/en-US.json
@@ -459,7 +459,7 @@
     "message": "{itemCount, plural,\n    =1 {1 chain found}\n    other {# chains found}\n    }"
   },
   "lighthouse-core/audits/critical-request-chains.js | title": {
-    "message": "Minimize Critical Requests Depth"
+    "message": "Avoid chaining critical requests"
   },
   "lighthouse-core/audits/deprecations.js | columnDeprecate": {
     "message": "Deprecation / Warning"
@@ -522,7 +522,7 @@
     "message": "Value"
   },
   "lighthouse-core/audits/dobetterweb/dom-size.js | description": {
-    "message": "Browser engineers recommend pages contain fewer than ~1,500 DOM elements. The sweet spot is a tree depth < 32 elements and fewer than 60 children/parent element. A large DOM can increase memory usage, cause longer [style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://web.dev/dom-size)."
+    "message": "A large DOM will increase memory usage, cause longer [style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://web.dev/dom-size)."
   },
   "lighthouse-core/audits/dobetterweb/dom-size.js | displayValue": {
     "message": "{itemCount, plural,\n    =1 {1 element}\n    other {# elements}\n    }"
@@ -1089,7 +1089,7 @@
     "message": "Reduce the impact of third-party code"
   },
   "lighthouse-core/audits/third-party-summary.js | title": {
-    "message": "Third-Party usage"
+    "message": "Minimize third-party usage"
   },
   "lighthouse-core/audits/time-to-first-byte.js | description": {
     "message": "Time To First Byte identifies the time at which your server sends a response. [Learn more](https://web.dev/time-to-first-byte)."
diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json
index 0ac673eda..53e28e2ac 100644
--- a/lighthouse-core/lib/i18n/locales/en-XL.json
+++ b/lighthouse-core/lib/i18n/locales/en-XL.json
@@ -459,7 +459,7 @@
     "message": "{itemCount, plural,\n    =1 {1 ĉh́âín̂ f́ôún̂d́}\n    other {# ĉh́âín̂ś f̂óûńd̂}\n    }"
   },
   "lighthouse-core/audits/critical-request-chains.js | title": {
-    "message": "M̂ín̂ím̂íẑé Ĉŕît́îćâĺ R̂éq̂úêśt̂ś D̂ép̂t́ĥ"
+    "message": "Âv́ôíd̂ ćĥáîńîńĝ ćr̂ít̂íĉál̂ ŕêq́ûéŝt́ŝ"
   },
   "lighthouse-core/audits/deprecations.js | columnDeprecate": {
     "message": "D̂ép̂ŕêćât́îón̂ / Ẃâŕn̂ín̂ǵ"
@@ -522,7 +522,7 @@
     "message": "V̂ál̂úê"
   },
   "lighthouse-core/audits/dobetterweb/dom-size.js | description": {
-    "message": "B̂ŕôẃŝér̂ én̂ǵîńêér̂ś r̂éĉóm̂ḿêńd̂ ṕâǵêś ĉón̂t́âín̂ f́êẃêŕ t̂h́âń ~1,500 D̂ÓM̂ él̂ém̂én̂t́ŝ. T́ĥé ŝẃêét̂ śp̂ót̂ íŝ á t̂ŕêé d̂ép̂t́ĥ < 32 él̂ém̂én̂t́ŝ án̂d́ f̂éŵér̂ t́ĥán̂ 60 ćĥíl̂d́r̂én̂/ṕâŕêńt̂ él̂ém̂én̂t́. Â ĺâŕĝé D̂ÓM̂ ćâń îńĉŕêáŝé m̂ém̂ór̂ý ûśâǵê, ćâúŝé l̂ón̂ǵêŕ [ŝt́ŷĺê ćâĺĉúl̂át̂íôńŝ](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), án̂d́ p̂ŕôd́ûćê ćôśt̂ĺŷ [ĺâýôút̂ ŕêf́l̂óŵś](https://developers.google.com/speed/articles/reflow). [L̂éâŕn̂ ḿôŕê](https://web.dev/dom-size)."
+    "message": "Â ĺâŕĝé D̂ÓM̂ ẃîĺl̂ ín̂ćr̂éâśê ḿêḿôŕŷ úŝáĝé, ĉáûśê ĺôńĝér̂ [śt̂ýl̂é ĉál̂ćûĺât́îón̂ś](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), âńd̂ ṕr̂ód̂úĉé ĉóŝt́l̂ý [l̂áŷóût́ r̂éf̂ĺôẃŝ](https://developers.google.com/speed/articles/reflow). [Ĺêár̂ń m̂ór̂é](https://web.dev/dom-size)."
   },
   "lighthouse-core/audits/dobetterweb/dom-size.js | displayValue": {
     "message": "{itemCount, plural,\n    =1 {1 êĺêḿêńt̂}\n    other {# él̂ém̂én̂t́ŝ}\n    }"
@@ -1089,7 +1089,7 @@
     "message": "R̂éd̂úĉé t̂h́ê ím̂ṕâćt̂ óf̂ t́ĥír̂d́-p̂ár̂t́ŷ ćôd́ê"
   },
   "lighthouse-core/audits/third-party-summary.js | title": {
-    "message": "T̂h́îŕd̂-Ṕâŕt̂ý ûśâǵê"
+    "message": "M̂ín̂ím̂íẑé t̂h́îŕd̂-ṕâŕt̂ý ûśâǵê"
   },
   "lighthouse-core/audits/time-to-first-byte.js | description": {
     "message": "T̂ím̂é T̂ó F̂ír̂śt̂ B́ŷt́ê íd̂én̂t́îf́îéŝ t́ĥé t̂ím̂é ât́ ŵh́îćĥ ýôúr̂ śêŕv̂ér̂ śêńd̂ś â ŕêśp̂ón̂śê. [Ĺêár̂ń m̂ór̂é](https://web.dev/time-to-first-byte)."
diff --git a/package.json b/package.json
index 08a421912..730aab34a 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
     "debug": "node --inspect-brk ./lighthouse-cli/index.js",
     "start": "node ./lighthouse-cli/index.js",
     "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check",
+    "test-bundle": "node build/tests/bundle-smoke-test.js",
     "test-clients": "jest \"clients/\"",
     "test-viewer": "yarn unit-viewer && jest lighthouse-viewer/test/viewer-test-pptr.js",
     "test-lantern": "bash lighthouse-core/scripts/test-lantern.sh",
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index 63b56c509..0df7875e3 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -375,7 +375,7 @@
             "id": "critical-request-chains",
             "score": null,
             "scoreDisplayMode": "informative",
-            "title": "Minimize Critical Requests Depth"
+            "title": "Avoid chaining critical requests"
         },
         "custom-controls-labels": {
             "description": "Custom interactive controls have associated labels, provided by aria-label or aria-labelledby. [Learn more](https://web.dev/custom-controls-labels/).",
@@ -502,7 +502,7 @@
             "title": "Document has a `<title>` element"
         },
         "dom-size": {
-            "description": "Browser engineers recommend pages contain fewer than ~1,500 DOM elements. The sweet spot is a tree depth < 32 elements and fewer than 60 children/parent element. A large DOM can increase memory usage, cause longer [style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://web.dev/dom-size).",
+            "description": "A large DOM will increase memory usage, cause longer [style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://web.dev/dom-size).",
             "details": {
                 "headings": [
                     {
@@ -2607,7 +2607,7 @@
             "id": "third-party-summary",
             "score": 1.0,
             "scoreDisplayMode": "binary",
-            "title": "Third-Party usage"
+            "title": "Minimize third-party usage"
         },
         "time-to-first-byte": {
             "description": "Time To First Byte identifies the time at which your server sends a response. [Learn more](https://web.dev/time-to-first-byte).",
diff --git a/types/smokehouse.d.ts b/types/smokehouse.d.ts
index a512b481d..4f6863ecd 100644
--- a/types/smokehouse.d.ts
+++ b/types/smokehouse.d.ts
@@ -41,4 +41,11 @@
     config: LH.Config.Json;
     batch: string;
   }
+
+  export interface FirehouseOptions {
+    runLighthouse: (url: string, config: LH.Config.Json) => Promise<Omit<LH.RunnerResult, 'report'>>;
+    urlFilterRegex?: RegExp;
+    skip?: (test: TestDfn, expectation: ExpectedRunnerResult) => string | false;
+    modify?: (test: TestDfn, expectation: ExpectedRunnerResult) => void;
+  }
 }


Test patch:
diff --git a/build/tests/bundle-smoke-test.js b/build/tests/bundle-smoke-test.js
new file mode 100644
index 000000000..59dbc076d
--- /dev/null
+++ b/build/tests/bundle-smoke-test.js
@@ -0,0 +1,66 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const fs = require('fs');
+const {promisify} = require('util');
+const {execFile} = require('child_process');
+const execFileAsync = promisify(execFile);
+const firehouse = require('../../lighthouse-cli/test/smokehouse/firehouse.js');
+const bundleBuilder = require('../build-bundle.js');
+const {server, serverForOffline} = require('../../lighthouse-cli/test/fixtures/static-server.js');
+
+const testEntryPath = `${__dirname}/../../lighthouse-core/index.js`;
+const testDistPath = `${__dirname}/../../dist/test-bundle.js`;
+
+/**
+ * Run Lighthouse using a CLI that shims lighthouse-core with the output of the bundler.
+ * @param {string} url
+ * @param {LH.Config.Json} config
+ */
+async function runLighthouseFromMinifiedBundle(url, config) {
+  const configPath = `${__dirname}/../../.tmp/bundle-smoke-test-config.json`;
+  const lhrPath = `${__dirname}/../../.tmp/bundle-smoke-test-lhr.json`;
+  const gatherPath = `${__dirname}/../../.tmp/bundle-smoke-test-gather`;
+
+  fs.writeFileSync(configPath, JSON.stringify(config));
+
+  await execFileAsync('node', [
+    `${__dirname}/bundled-lighthouse-cli.js`,
+    url,
+    `--config-path=${configPath}`,
+    `-GA=${gatherPath}`,
+    '--output=json',
+    `--output-path=${lhrPath}`,
+  ]);
+
+  const lhr = JSON.parse(fs.readFileSync(lhrPath, 'utf-8'));
+  const artifacts = JSON.parse(fs.readFileSync(`${gatherPath}/artifacts.json`, 'utf-8'));
+
+  return {
+    lhr,
+    artifacts,
+  };
+}
+
+async function main() {
+  await bundleBuilder.build(testEntryPath, testDistPath);
+
+  server.listen(10200, 'localhost');
+  serverForOffline.listen(10503, 'localhost');
+
+  const results = await firehouse.runSmokes({
+    runLighthouse: runLighthouseFromMinifiedBundle,
+    urlFilterRegex: /byte|dbw/,
+  });
+
+  await new Promise(resolve => server.close(resolve));
+  await new Promise(resolve => serverForOffline.close(resolve));
+
+  process.exit(results.success ? 0 : 1);
+}
+
+main();
diff --git a/build/tests/bundled-lighthouse-cli.js b/build/tests/bundled-lighthouse-cli.js
new file mode 100644
index 000000000..2b4958e33
--- /dev/null
+++ b/build/tests/bundled-lighthouse-cli.js
@@ -0,0 +1,104 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+/**
+ * @fileoverview Used to smoke test the build process.
+ *
+ * - The bundled code is a function that, given a module ID, returns the exports of that module.
+ * - We eval the bundle string to get a reference to this function (with some global hacks to
+ *   support unbundleable things).
+ * - We try to locate the lighthouse-core/index.js module by executing this function on every
+ *   possible number. This version of lighthouse-core/index.js will be wired to use all of the
+ *   bundled modules, not node requires.
+ * - Once we find the bundled lighthouse-core/index.js module, we stick it in node's require.cache
+ *   so that all node require invocations for lighthouse-core/index.js will use our bundled module
+ *   instead of the regular one.
+ * - Finally, we kick off the lighthouse-cli/index.js entrypoint that ends up requiring the
+ *   now-replaced lighthouse-core/index.js for its run.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const mkdirp = require('mkdirp');
+const rimraf = require('rimraf');
+const ChromeProtocol = require('../../lighthouse-core/gather/connections/cri.js');
+
+const LH_ROOT = path.resolve(__dirname, '../..');
+const corePath = `${LH_ROOT}/lighthouse-core/index.js`;
+
+// Oh yeahhhhh this works. Think of this as `requireBundled('../../lighthouse-core/index.js')`.
+const lighthouse = (function getLighthouseCoreBundled() {
+  // The eval will assign to `require`. Normally, that would be the require on the global object.
+  // This `let` protects the global reference to the native require.
+  // Doesn't need to have any value, but for good measure define a function that explicitly forbids
+  // its own usage.
+  // To be further convinced that this works (that the un-bundled files are not being loaded),
+  // add some console.log's somewhere like `driver.js`, and
+  // run `node build/tests/bundled-lighthouse-cli.js https://www.example.com`. You won't see them.
+  /* eslint-disable-next-line */
+  let require = () => {
+    throw new Error('illegal require');
+  };
+
+  const lighthouseBundledCode = fs.readFileSync('dist/test-bundle.js', 'utf-8')
+    // Some modules are impossible to bundle. So we cheat by leaning on global.
+    // cri.js will be able to use native require. It's a minor defect - it means that some usages
+    // of lh-error.js will not come from the bundled code.
+    // TODO: use `globalThis` when we drop Node 10.
+    .replace('new ChromeProtocol', 'new global.ChromeProtocol')
+    // Needed for asset-saver.js.
+    .replace(/mkdirp\./g, 'global.mkdirp.')
+    .replace(/rimraf\./g, 'global.rimraf.')
+    .replace(/fs\.(writeFileSync|createWriteStream)/g, 'global.$&');
+
+  /* eslint-disable no-undef */
+  // @ts-ignore
+  global.ChromeProtocol = ChromeProtocol;
+  // @ts-ignore
+  global.mkdirp = mkdirp;
+  // @ts-ignore
+  global.rimraf = rimraf;
+  // @ts-ignore
+  global.fs = fs;
+  /*  eslint-enable no-undef */
+
+  const bundledLighthouseRequire = eval(lighthouseBundledCode);
+
+  // Find the lighthouse module.
+  // Every module is given an id (starting at 1). The core lighthouse module
+  // is the only module that is a function named `lighthouse`.
+  /** @type {import('../../lighthouse-core/index.js')} */
+  let lighthouse;
+  for (let i = 1; i < 1000; i++) {
+    const module = bundledLighthouseRequire(i);
+    if (module.name === 'lighthouse') {
+      lighthouse = module;
+      break;
+    }
+  }
+
+  // @ts-ignore
+  if (!lighthouse) throw new Error('could not find lighthouse module');
+
+  return lighthouse;
+})();
+
+// Shim the core module with the bundled code.
+
+// @ts-ignore
+lighthouse.__PATCHED__ = true;
+require.cache[corePath] = {
+  exports: lighthouse,
+};
+
+// @ts-ignore
+if (!require('../../lighthouse-core/index.js').__PATCHED__) {
+  throw new Error('error patching core module');
+}
+
+// Kick off the CLI.
+require('../../lighthouse-cli/index.js');
diff --git a/lighthouse-cli/test/smokehouse/firehouse.js b/lighthouse-cli/test/smokehouse/firehouse.js
new file mode 100644
index 000000000..10f7309ca
--- /dev/null
+++ b/lighthouse-cli/test/smokehouse/firehouse.js
@@ -0,0 +1,68 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+/**
+ * @fileoverview Smoke test runner.
+ * Used to test channels other than npm (`run-smoke.js` handles that).
+ * Supports skipping and modifiying expectations to match the environment.
+ */
+
+/* eslint-disable no-console */
+
+const log = require('lighthouse-logger');
+const smokeTests = require('./smoke-test-dfns.js');
+const {collateResults, report} = require('./smokehouse-report.js');
+
+/**
+ * @param {Smokehouse.FirehouseOptions} options
+ */
+async function runSmokes(options) {
+  const {runLighthouse, urlFilterRegex, skip, modify} = options;
+
+  let passingCount = 0;
+  let failingCount = 0;
+
+  for (const test of smokeTests) {
+    for (const expected of test.expectations) {
+      if (urlFilterRegex && !expected.lhr.requestedUrl.match(urlFilterRegex)) {
+        continue;
+      }
+
+      console.log(`====== ${expected.lhr.requestedUrl} ======`);
+      const reasonToSkip = skip && skip(test, expected);
+      if (reasonToSkip) {
+        console.log(`skipping: ${reasonToSkip}`);
+        continue;
+      }
+
+      modify && modify(test, expected);
+      const results = await runLighthouse(expected.lhr.requestedUrl, test.config);
+      console.log(`Asserting expected results match those found. (${expected.lhr.requestedUrl})`);
+      const collated = collateResults(results, expected);
+      const counts = report(collated);
+      passingCount += counts.passed;
+      failingCount += counts.failed;
+    }
+  }
+
+  if (passingCount) {
+    console.log(log.greenify(`${passingCount} passing`));
+  }
+  if (failingCount) {
+    console.log(log.redify(`${failingCount} failing`));
+  }
+
+  return {
+    success: passingCount > 0 && failingCount === 0,
+    passingCount,
+    failingCount,
+  };
+}
+
+module.exports = {
+  runSmokes,
+};
diff --git a/lighthouse-core/test/audits/dobetterweb/dom-size-test.js b/lighthouse-core/test/audits/dobetterweb/dom-size-test.js
index 509568c44..67c7bb457 100644
--- a/lighthouse-core/test/audits/dobetterweb/dom-size-test.js
+++ b/lighthouse-core/test/audits/dobetterweb/dom-size-test.js
@@ -12,7 +12,7 @@ const options = DOMSize.defaultOptions;
 /* eslint-env jest */
 
 describe('DOMSize audit', () => {
-  const numElements = DOMSize.MAX_DOM_ELEMENTS;
+  const numElements = 1500;
   const artifact = {
     DOMStats: {
       totalBodyElements: numElements,
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index c3ba3a1f9..fe2621605 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -306,7 +306,7 @@
     },
     "critical-request-chains": {
       "id": "critical-request-chains",
-      "title": "Minimize Critical Requests Depth",
+      "title": "Avoid chaining critical requests",
       "description": "The Critical Request Chains below show you what resources are loaded with a high priority. Consider reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load. [Learn more](https://web.dev/critical-request-chains).",
       "score": null,
       "scoreDisplayMode": "informative",
@@ -1484,7 +1484,7 @@
     },
     "third-party-summary": {
       "id": "third-party-summary",
-      "title": "Third-Party usage",
+      "title": "Minimize third-party usage",
       "description": "Third-party code can significantly impact load performance. Limit the number of redundant third-party providers and try to load third-party code after your page has primarily finished loading. [Learn more](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript/).",
       "score": 1,
       "scoreDisplayMode": "binary",
@@ -2648,7 +2648,7 @@
     "dom-size": {
       "id": "dom-size",
       "title": "Avoids an excessive DOM size",
-      "description": "Browser engineers recommend pages contain fewer than ~1,500 DOM elements. The sweet spot is a tree depth < 32 elements and fewer than 60 children/parent element. A large DOM can increase memory usage, cause longer [style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://web.dev/dom-size).",
+      "description": "A large DOM will increase memory usage, cause longer [style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://web.dev/dom-size).",
       "score": 1,
       "scoreDisplayMode": "numeric",
       "numericValue": 31,