Repository: GoogleChrome/lighthouse
Pull request #10305 (2020-02-07 00:42:06)

viewer(css): adding grayscale & wait cursor while loading
fixes https://github.com/GoogleChrome/lighthouse/issues/10010


Patch:
diff --git a/lighthouse-core/computed/origin-summary.js b/lighthouse-core/computed/origin-summary.js
new file mode 100644
index 000000000..0c63875ca
--- /dev/null
+++ b/lighthouse-core/computed/origin-summary.js
@@ -0,0 +1,50 @@
+/**
+ * @license Copyright 2020 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 makeComputedArtifact = require('./computed-artifact.js');
+const NetworkRecords = require('./network-records.js');
+const URL = require('../lib/url-shim.js');
+
+/** @typedef {{count: number, size: number}} ResourceEntry */
+class OriginSummary {
+  /**
+   * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
+   * @return {Record<string,ResourceEntry>}
+   */
+  static summarize(networkRecords) {
+    /** @type {Record<string,ResourceEntry>} */
+    const resourceSummary = {
+      total: {count: 0, size: 0},
+    };
+
+    for (const record of networkRecords) {
+      const origin = URL.getOrigin(record.url);
+      if (!origin) continue;
+      if (!resourceSummary[origin]) {
+        resourceSummary[origin] = {count: 0, size: 0};
+      }
+      resourceSummary[origin].count++;
+      resourceSummary[origin].size += record.transferSize;
+
+      resourceSummary.total.count++;
+      resourceSummary.total.size += record.transferSize;
+    }
+    return resourceSummary;
+  }
+
+  /**
+   * @param {LH.DevtoolsLog} devtoolsLog
+   * @param {LH.Audit.Context} context
+   * @return {Promise<Record<string,ResourceEntry>>}
+   */
+  static async compute_(devtoolsLog, context) {
+    const networkRecords = await NetworkRecords.request(devtoolsLog, context);
+    return OriginSummary.summarize(networkRecords);
+  }
+}
+
+module.exports = makeComputedArtifact(OriginSummary);
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index c479d2bff..15eba06b4 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -215,6 +215,7 @@ const defaultConfig = {
     'metrics',
     'offline-start-url',
     'performance-budget',
+    'timing-budget',
     'resource-summary',
     'third-party-summary',
     'manual/pwa-cross-browser',
@@ -417,6 +418,7 @@ const defaultConfig = {
         {id: 'mainthread-work-breakdown', weight: 0, group: 'diagnostics'},
         {id: 'font-display', weight: 0, group: 'diagnostics'},
         {id: 'performance-budget', weight: 0, group: 'budgets'},
+        {id: 'timing-budget', weight: 0, group: 'budgets'},
         {id: 'resource-summary', weight: 0, group: 'diagnostics'},
         {id: 'third-party-summary', weight: 0, group: 'diagnostics'},
         // Audits past this point don't belong to a group and will not be shown automatically
diff --git a/lighthouse-core/config/source-maps-config.js b/lighthouse-core/config/source-maps-config.js
new file mode 100644
index 000000000..0bf69aa97
--- /dev/null
+++ b/lighthouse-core/config/source-maps-config.js
@@ -0,0 +1,37 @@
+/**
+ * @license Copyright 2020 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 Temporary config for new source map features.
+ * Allows for source map audits to land in master without being enabled by default.
+ */
+
+/** @type {LH.Config.Json} */
+const config = {
+  extends: 'lighthouse:default',
+  passes: [{
+    passName: 'defaultPass',
+    gatherers: [
+      'source-maps',
+    ],
+  }],
+  audits: [
+    // About to be added.
+    // 'bundle-duplication',
+  ],
+  // @ts-ignore: `title` is required in CategoryJson. setting to the same value as the default
+  // config is awkward - easier to omit the property here. Will defer to default config.
+  categories: {
+    'performance': {
+      auditRefs: [
+        // About to be added.
+        // {id: 'bundle-duplication', weight: 0, group: 'load-opportunities'},
+      ],
+    },
+  },
+};
+
+module.exports = config;
diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js
index cde391dda..c0c593088 100644
--- a/lighthouse-core/report/html/renderer/performance-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js
@@ -167,17 +167,24 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
     }
 
     // Budgets
-    const budgetAudit = category.auditRefs.find(audit => audit.id === 'performance-budget');
-    if (budgetAudit && budgetAudit.result.details) {
-      const table = this.detailsRenderer.render(budgetAudit.result.details);
-      if (table) {
-        table.id = budgetAudit.id;
-        table.classList.add('lh-audit');
-        const budgetsGroupEl = this.renderAuditGroup(groups.budgets);
-        budgetsGroupEl.appendChild(table);
-        budgetsGroupEl.classList.add('lh-audit-group--budgets');
-        element.appendChild(budgetsGroupEl);
+    /** @type {Array<Element>} */
+    const budgetTableEls = [];
+    ['performance-budget', 'timing-budget'].forEach((id) => {
+      const audit = category.auditRefs.find(audit => audit.id === id);
+      if (audit && audit.result.details) {
+        const table = this.detailsRenderer.render(audit.result.details);
+        if (table) {
+          table.id = id;
+          table.classList.add('lh-audit');
+          budgetTableEls.push(table);
+        }
       }
+    });
+    if (budgetTableEls.length > 0) {
+      const budgetsGroupEl = this.renderAuditGroup(groups.budgets);
+      budgetTableEls.forEach(table => budgetsGroupEl.appendChild(table));
+      budgetsGroupEl.classList.add('lh-audit-group--budgets');
+      element.appendChild(budgetsGroupEl);
     }
 
     // Opportunities
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 69ed7c54a..5ebc13306 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -860,8 +860,9 @@
 }
 
 /* Style the "over budget" columns red. */
-.lh-audit-group--budgets .lh-table tbody tr td:nth-child(4),
-.lh-audit-group--budgets .lh-table tbody tr td:nth-child(5){
+.lh-audit-group--budgets #performance-budget tbody tr td:nth-child(4),
+.lh-audit-group--budgets #performance-budget tbody tr td:nth-child(5),
+.lh-audit-group--budgets #timing-budget tbody tr td:nth-child(3) {
   color: var(--color-red-700);
 }
 
@@ -872,6 +873,7 @@
 
 .lh-audit-group--budgets .lh-table {
   width: 100%;
+  margin: 16px 0px 16px 0px;
 }
 
 .lh-audit-group--pwa-fast-reliable .lh-audit-group__header::before {
diff --git a/lighthouse-viewer/app/styles/viewer.css b/lighthouse-viewer/app/styles/viewer.css
index ff522e797..de5fb0251 100644
--- a/lighthouse-viewer/app/styles/viewer.css
+++ b/lighthouse-viewer/app/styles/viewer.css
@@ -61,7 +61,9 @@
   background-color: #fff;
 }
 .viewer-placeholder-inner.lh-loading {
-  filter: blur(2px);
+  filter: blur(2px) grayscale(1);
+  pointer-events: none;
+  cursor: wait
 }
 .viewer-placeholder-inner.dropping {
   border-color: currentColor;
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index 0ae21947b..cb3526ed5 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -2749,6 +2749,65 @@
             "scoreDisplayMode": "binary",
             "title": "Server response times are low (TTFB)"
         },
+        "timing-budget": {
+            "description": "Set a timing budget to help you keep an eye on the performance of your site. Performant sites load fast and respond to user input events quickly. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/budgets).",
+            "details": {
+                "headings": [
+                    {
+                        "itemType": "text",
+                        "key": "label",
+                        "text": "Metric"
+                    },
+                    {
+                        "itemType": "ms",
+                        "key": "measurement",
+                        "text": "Measurement"
+                    },
+                    {
+                        "itemType": "ms",
+                        "key": "overBudget",
+                        "text": "Over Budget"
+                    }
+                ],
+                "items": [
+                    {
+                        "label": "First CPU Idle",
+                        "measurement": 4927.0,
+                        "metric": "first-cpu-idle",
+                        "overBudget": 2027.0
+                    },
+                    {
+                        "label": "Time to Interactive",
+                        "measurement": 4927.0,
+                        "metric": "interactive",
+                        "overBudget": 2027.0
+                    },
+                    {
+                        "label": "First Meaningful Paint",
+                        "measurement": 3969.0,
+                        "metric": "first-meaningful-paint",
+                        "overBudget": 1969.0
+                    },
+                    {
+                        "label": "First Contentful Paint",
+                        "measurement": 3969.0,
+                        "metric": "first-contentful-paint",
+                        "overBudget": 969.0
+                    },
+                    {
+                        "label": "Max Potential First Input Delay",
+                        "measurement": 123.0,
+                        "metric": "max-potential-fid",
+                        "overBudget": 23.0
+                    }
+                ],
+                "type": "table"
+            },
+            "id": "timing-budget",
+            "score": null,
+            "scoreDisplayMode": "informative",
+            "title": "Timing budget"
+        },
         "total-blocking-time": {
             "description": "Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. [Learn more](https://web.dev/lighthouse-total-blocking-time).",
             "displayValue": "120\u00a0ms",
@@ -3862,6 +3921,11 @@
                     "id": "performance-budget",
                     "weight": 0.0
                 },
+                {
+                    "group": "budgets",
+                    "id": "timing-budget",
+                    "weight": 0.0
+                },
                 {
                     "group": "diagnostics",
                     "id": "resource-summary",
@@ -4706,6 +4770,12 @@
                 "name": "lh:computed:ResourceSummary",
                 "startTime": 0.0
             },
+            {
+                "duration": 100.0,
+                "entryType": "measure",
+                "name": "lh:audit:timing-budget",
+                "startTime": 0.0
+            },
             {
                 "duration": 100.0,
                 "entryType": "measure",


Test patch:
diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
index 41d7d171c..ea4a32e6f 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -132,6 +132,9 @@ Object {
     Object {
       "path": "performance-budget",
     },
+    Object {
+      "path": "timing-budget",
+    },
     Object {
       "path": "resource-summary",
     },
@@ -900,6 +903,11 @@ Object {
           "id": "performance-budget",
           "weight": 0,
         },
+        Object {
+          "group": "budgets",
+          "id": "timing-budget",
+          "weight": 0,
+        },
         Object {
           "group": "diagnostics",
           "id": "resource-summary",
diff --git a/lighthouse-core/test/computed/origin-summary-test.js b/lighthouse-core/test/computed/origin-summary-test.js
new file mode 100644
index 000000000..243fcafd5
--- /dev/null
+++ b/lighthouse-core/test/computed/origin-summary-test.js
@@ -0,0 +1,87 @@
+/**
+ * @license Copyright 2020 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 ComputedOriginSummary = require('../../computed/origin-summary.js');
+const assert = require('assert');
+const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
+
+/* eslint-env jest */
+
+describe('Origin summary computed', () => {
+  let artifacts;
+  let context;
+  beforeEach(() => {
+    context = {computedCache: new Map()};
+  });
+
+  it('computes statistics for the page overall', async () => {
+    artifacts = networkRecordsToDevtoolsLog([
+      {url: 'http://example.com/file.html', resourceType: 'Document', transferSize: 30},
+      {url: 'https://foo.bar.com/script.js', resourceType: 'Script', transferSize: 50},
+      {url: 'http://filez.com/file.jpg', resourceType: 'Image', transferSize: 70},
+    ]);
+    const result = await ComputedOriginSummary.request(artifacts, context);
+    assert.equal(result['total'].count, 3);
+    assert.equal(result['total'].size, 150);
+  });
+
+  it('computes statistics for each origin', async () => {
+    artifacts = networkRecordsToDevtoolsLog([
+      {url: 'http://example.com/file.html', resourceType: 'Document', transferSize: 30},
+      {url: 'http://foo.bar.com/script.js', resourceType: 'Script', transferSize: 50},
+      {url: 'http://foo.bar.com/file.jpg', resourceType: 'Image', transferSize: 70},
+      {url: 'https://foo.bar.com/SECURE.jpg', resourceType: 'Image', transferSize: 7},
+    ]);
+    const result = await ComputedOriginSummary.request(artifacts, context);
+    assert.equal(result['http://example.com'].count, 1);
+    assert.equal(result['http://example.com'].size, 30);
+
+    // HTTP & HTTPS version of the same domain are two different origins
+    assert.equal(result['http://foo.bar.com'].count, 2);
+    assert.equal(result['http://foo.bar.com'].size, 120);
+
+    assert.equal(result['https://foo.bar.com'].count, 1);
+    assert.equal(result['https://foo.bar.com'].size, 7);
+  });
+
+  it('computes statistics per origin rather than per domain', async () => {
+    artifacts = networkRecordsToDevtoolsLog([
+      {url: 'http://example.com/file.html', resourceType: 'Document', transferSize: 30},
+      {url: 'https://example.com/file1.html', resourceType: 'Document', transferSize: 75},
+      {url: 'http://subdomain.example.com/another-file.html', resourceType: 'Document', transferSize: 50},
+    ]);
+
+    const result = await ComputedOriginSummary.request(artifacts, context);
+    assert.equal(result['http://example.com'].count, 1);
+    assert.equal(result['http://example.com'].size, 30);
+
+    assert.equal(result['https://example.com'].count, 1);
+    assert.equal(result['https://example.com'].size, 75);
+
+    assert.equal(result['http://subdomain.example.com'].count, 1);
+    assert.equal(result['http://subdomain.example.com'].size, 50);
+  });
+
+  /**
+   * Data URLs should be ignored to avoid double counting.
+   * The filesize of a data URL is reflected in the filesize of the resource that contains it.
+   * A data URL does not result in a separate network request.
+   */
+  it('ignores data URLs', async () => {
+    artifacts = networkRecordsToDevtoolsLog([
+      {url: 'http://example.com/file.html', resourceType: 'Document', transferSize: 30},
+      {url: 'data:image/png;base64,iVBORw0KGgoAA', resourceType: 'Image', transferSize: 10},
+    ]);
+
+    const result = await ComputedOriginSummary.request(artifacts, context);
+    assert.equal(result['http://example.com'].count, 1);
+    assert.equal(result['http://example.com'].size, 30);
+
+    assert.equal(result['total'].count, 1);
+    assert.equal(result['total'].size, 30);
+  });
+});
diff --git a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
index 5a3c99e6c..233870acd 100644
--- a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
@@ -190,7 +190,7 @@ describe('PerfCategoryRenderer', () => {
   });
 
   describe('budgets', () => {
-    it('renders a performance budget', () => {
+    it('renders the group and header', () => {
       const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
 
       const budgetsGroup = categoryDOM.querySelector('.lh-audit-group.lh-audit-group--budgets');
@@ -198,8 +198,11 @@ describe('PerfCategoryRenderer', () => {
 
       const header = budgetsGroup.querySelector('.lh-audit-group__header');
       assert.ok(header);
+    });
 
-      const budgetTable = budgetsGroup.querySelector('#performance-budget.lh-table');
+    it('renders the performance budget table', () => {
+      const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
+      const budgetTable = categoryDOM.querySelector('#performance-budget.lh-table');
       assert.ok(budgetTable);
 
       const lhrBudgetEntries = sampleResults.audits['performance-budget'].details.items;
@@ -207,11 +210,23 @@ describe('PerfCategoryRenderer', () => {
       assert.strictEqual(tableRows.length, lhrBudgetEntries.length);
     });
 
-    it('does not render a budget table when performance-budget audit is notApplicable', () => {
+    it('renders the timing budget table', () => {
+      const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
+      const budgetTable = categoryDOM.querySelector('#timing-budget.lh-table');
+      assert.ok(budgetTable);
+
+      const lhrBudgetEntries = sampleResults.audits['timing-budget'].details.items;
+      const tableRows = budgetTable.querySelectorAll('tbody > tr');
+      assert.strictEqual(tableRows.length, lhrBudgetEntries.length);
+    });
+
+    it('does not render the budgets section when all budget audits are notApplicable', () => {
       const budgetlessCategory = JSON.parse(JSON.stringify(category));
-      const budgetRef = budgetlessCategory.auditRefs.find(a => a.id === 'performance-budget');
-      budgetRef.result.scoreDisplayMode = 'notApplicable';
-      delete budgetRef.result.details;
+      ['performance-budget', 'timing-budget'].forEach((id) => {
+        const budgetRef = budgetlessCategory.auditRefs.find(a => a.id === id);
+        budgetRef.result.scoreDisplayMode = 'notApplicable';
+        delete budgetRef.result.details;
+      });
 
       const categoryDOM = renderer.render(budgetlessCategory, sampleResults.categoryGroups);
       const budgetsGroup = categoryDOM.querySelector('.lh-audit-group.lh-audit-group--budgets');
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 695da3eca..625d696a0 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -1429,6 +1429,65 @@
         ]
       }
     },
+    "timing-budget": {
+      "id": "timing-budget",
+      "title": "Timing budget",
+      "description": "Set a timing budget to help you keep an eye on the performance of your site. Performant sites load fast and respond to user input events quickly. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/budgets).",
+      "score": null,
+      "scoreDisplayMode": "informative",
+      "details": {
+        "type": "table",
+        "headings": [
+          {
+            "key": "label",
+            "itemType": "text",
+            "text": "Metric"
+          },
+          {
+            "key": "measurement",
+            "itemType": "ms",
+            "text": "Measurement"
+          },
+          {
+            "key": "overBudget",
+            "itemType": "ms",
+            "text": "Over Budget"
+          }
+        ],
+        "items": [
+          {
+            "metric": "first-cpu-idle",
+            "label": "First CPU Idle",
+            "measurement": 4927,
+            "overBudget": 2027
+          },
+          {
+            "metric": "interactive",
+            "label": "Time to Interactive",
+            "measurement": 4927,
+            "overBudget": 2027
+          },
+          {
+            "metric": "first-meaningful-paint",
+            "label": "First Meaningful Paint",
+            "measurement": 3969,
+            "overBudget": 1969
+          },
+          {
+            "metric": "first-contentful-paint",
+            "label": "First Contentful Paint",
+            "measurement": 3969,
+            "overBudget": 969
+          },
+          {
+            "metric": "max-potential-fid",
+            "label": "Max Potential First Input Delay",
+            "measurement": 123,
+            "overBudget": 23
+          }
+        ]
+      }
+    },
     "resource-summary": {
       "id": "resource-summary",
       "title": "Keep request counts low and transfer sizes small",
@@ -3708,6 +3767,11 @@
           "weight": 0,
           "group": "budgets"
         },
+        {
+          "id": "timing-budget",
+          "weight": 0,
+          "group": "budgets"
+        },
         {
           "id": "resource-summary",
           "weight": 0,
@@ -4791,6 +4855,12 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:audit:timing-budget",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:resource-summary",
@@ -5471,7 +5541,8 @@
         "audits[without-javascript].description"
       ],
       "lighthouse-core/lib/i18n/i18n.js | firstContentfulPaintMetric": [
-        "audits[first-contentful-paint].title"
+        "audits[first-contentful-paint].title",
+        "audits[timing-budget].details.items[3].label"
       ],
       "lighthouse-core/audits/metrics/first-contentful-paint.js | description": [
         "audits[first-contentful-paint].description"
@@ -5527,7 +5598,8 @@
         }
       ],
       "lighthouse-core/lib/i18n/i18n.js | firstMeaningfulPaintMetric": [
-        "audits[first-meaningful-paint].title"
+        "audits[first-meaningful-paint].title",
+        "audits[timing-budget].details.items[2].label"
       ],
       "lighthouse-core/audits/metrics/first-meaningful-paint.js | description": [
         "audits[first-meaningful-paint].description"
@@ -5595,7 +5667,8 @@
         "audits[total-blocking-time].description"
       ],
       "lighthouse-core/lib/i18n/i18n.js | maxPotentialFIDMetric": [
-        "audits[max-potential-fid].title"
+        "audits[max-potential-fid].title",
+        "audits[timing-budget].details.items[4].label"
       ],
       "lighthouse-core/audits/metrics/max-potential-fid.js | description": [
         "audits[max-potential-fid].description"
@@ -5643,13 +5716,15 @@
         }
       ],
       "lighthouse-core/lib/i18n/i18n.js | firstCPUIdleMetric": [
-        "audits[first-cpu-idle].title"
+        "audits[first-cpu-idle].title",
+        "audits[timing-budget].details.items[0].label"
       ],
       "lighthouse-core/audits/metrics/first-cpu-idle.js | description": [
         "audits[first-cpu-idle].description"
       ],
       "lighthouse-core/lib/i18n/i18n.js | interactiveMetric": [
-        "audits.interactive.title"
+        "audits.interactive.title",
+        "audits[timing-budget].details.items[1].label"
       ],
       "lighthouse-core/audits/metrics/interactive.js | description": [
         "audits.interactive.description"
@@ -5823,7 +5898,8 @@
         "audits[resource-summary].details.headings[2].text"
       ],
       "lighthouse-core/lib/i18n/i18n.js | columnOverBudget": [
-        "audits[performance-budget].details.headings[4].text"
+        "audits[performance-budget].details.headings[4].text",
+        "audits[timing-budget].details.headings[2].text"
       ],
       "lighthouse-core/lib/i18n/i18n.js | scriptResourceType": [
         "audits[performance-budget].details.items[0].label",
@@ -5893,6 +5969,18 @@
         "audits[performance-budget].details.items[8].label",
         "audits[resource-summary].details.items[3].label"
       ],
+      "lighthouse-core/audits/timing-budget.js | title": [
+        "audits[timing-budget].title"
+      ],
+      "lighthouse-core/audits/timing-budget.js | description": [
+        "audits[timing-budget].description"
+      ],
+      "lighthouse-core/audits/timing-budget.js | columnTimingMetric": [
+        "audits[timing-budget].details.headings[0].text"
+      ],
+      "lighthouse-core/audits/timing-budget.js | columnMeasurement": [
+        "audits[timing-budget].details.headings[1].text"
+      ],
       "lighthouse-core/audits/resource-summary.js | title": [
         "audits[resource-summary].title"
       ],