lighthouse 8.6.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/report/bundle.esm.js +5994 -0
- package/dist/report/flow.js +30 -13
- package/dist/report/standalone.js +41 -35
- package/flow-report/.eslintrc.cjs +2 -0
- package/flow-report/assets/standalone-flow-template.html +3 -2
- package/flow-report/assets/styles.css +108 -31
- package/flow-report/src/app.tsx +18 -18
- package/flow-report/src/common.tsx +28 -15
- package/flow-report/src/header.tsx +12 -13
- package/flow-report/src/help-dialog.tsx +3 -4
- package/flow-report/src/i18n/i18n.tsx +57 -16
- package/flow-report/src/i18n/ui-strings.js +50 -0
- package/flow-report/src/icons.tsx +36 -8
- package/flow-report/src/sidebar/flow.tsx +3 -3
- package/flow-report/src/sidebar/sidebar.tsx +35 -14
- package/flow-report/src/summary/category.tsx +97 -24
- package/flow-report/src/summary/summary.tsx +21 -14
- package/flow-report/src/topbar.tsx +9 -5
- package/flow-report/src/util.ts +33 -26
- package/flow-report/src/wrappers/category-score.tsx +0 -3
- package/flow-report/src/wrappers/markdown.tsx +27 -0
- package/flow-report/src/wrappers/report-renderer.tsx +10 -3
- package/flow-report/src/wrappers/report.tsx +27 -6
- package/flow-report/standalone-flow.tsx +4 -3
- package/flow-report/test/common-test.tsx +32 -17
- package/flow-report/test/flow-report-pptr-test.ts +46 -0
- package/flow-report/test/header-test.tsx +6 -6
- package/flow-report/test/setup/env-setup.ts +1 -1
- package/flow-report/test/sidebar/sidebar-test.tsx +43 -1
- package/flow-report/test/summary/category-test.tsx +120 -24
- package/flow-report/test/summary/summary-test.tsx +1 -1
- package/flow-report/test/topbar-test.tsx +1 -0
- package/flow-report/test/util-test.tsx +32 -11
- package/flow-report/test/wrappers/category-score-test.tsx +87 -0
- package/flow-report/test/wrappers/markdown-test.tsx +29 -0
- package/flow-report/tsconfig.json +1 -0
- package/jest.config.js +4 -2
- package/lighthouse-cli/.eslintrc.cjs +2 -0
- package/lighthouse-cli/cli-flags.js +1 -1
- package/lighthouse-cli/run.js +1 -1
- package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +0 -0
- package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +4 -2
- package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
- package/lighthouse-cli/test/smokehouse/smokehouse.js +1 -1
- package/lighthouse-core/audits/accessibility/axe-audit.js +7 -1
- package/lighthouse-core/audits/audit.js +1 -1
- package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +2 -0
- package/lighthouse-core/audits/byte-efficiency/modern-image-formats.js +4 -2
- package/lighthouse-core/audits/byte-efficiency/offscreen-images.js +11 -10
- package/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js +5 -3
- package/lighthouse-core/audits/byte-efficiency/uses-responsive-images-snapshot.js +2 -1
- package/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +2 -1
- package/lighthouse-core/audits/deprecations.js +33 -11
- package/lighthouse-core/audits/dobetterweb/uses-http2.js +27 -11
- package/lighthouse-core/audits/errors-in-console.js +0 -2
- package/lighthouse-core/audits/image-aspect-ratio.js +5 -5
- package/lighthouse-core/audits/image-size-responsive.js +6 -6
- package/lighthouse-core/audits/installable-manifest.js +5 -3
- package/lighthouse-core/audits/preload-lcp-image.js +7 -5
- package/lighthouse-core/audits/script-treemap-data.js +1 -1
- package/lighthouse-core/audits/unsized-images.js +2 -3
- package/lighthouse-core/computed/image-records.js +4 -3
- package/lighthouse-core/computed/metrics/lantern-total-blocking-time.js +3 -4
- package/lighthouse-core/computed/metrics/tbt-utils.js +57 -0
- package/lighthouse-core/computed/metrics/total-blocking-time.js +3 -53
- package/lighthouse-core/computed/resource-summary.js +1 -1
- package/lighthouse-core/config/config-helpers.js +21 -10
- package/lighthouse-core/config/constants.js +3 -4
- package/lighthouse-core/config/default-config.js +61 -71
- package/lighthouse-core/fraggle-rock/config/config.js +1 -1
- package/lighthouse-core/fraggle-rock/config/default-config.js +4 -5
- package/lighthouse-core/fraggle-rock/gather/base-gatherer.js +6 -1
- package/lighthouse-core/fraggle-rock/gather/session.js +3 -4
- package/lighthouse-core/gather/connections/cri.js +1 -1
- package/lighthouse-core/gather/driver/storage.js +0 -1
- package/lighthouse-core/gather/driver.js +3 -7
- package/lighthouse-core/gather/fetcher.js +4 -7
- package/lighthouse-core/gather/gather-runner.js +29 -6
- package/lighthouse-core/gather/gatherers/accessibility.js +27 -0
- package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +1 -0
- package/lighthouse-core/gather/gatherers/image-elements.js +0 -3
- package/lighthouse-core/gather/gatherers/inspector-issues.js +5 -0
- package/lighthouse-core/lib/asset-saver.js +7 -4
- package/lighthouse-core/lib/emulation.js +45 -1
- package/lighthouse-core/lib/i18n/i18n.js +19 -8
- package/lighthouse-core/lib/lh-env.js +2 -0
- package/lighthouse-core/lib/proto-preprocessor.js +14 -2
- package/lighthouse-core/lib/tappable-rects.js +8 -15
- package/lighthouse-core/lib/url-shim.js +1 -1
- package/lighthouse-core/runner.js +1 -1
- package/lighthouse-core/util-commonjs.js +65 -46
- package/package.json +27 -17
- package/readme.md +3 -3
- package/report/.eslintrc.cjs +2 -0
- package/report/README.md +1 -1
- package/report/assets/standalone-template.html +2 -3
- package/report/assets/styles.css +360 -254
- package/report/assets/templates.html +21 -76
- package/report/clients/bundle.js +1 -0
- package/report/clients/standalone.js +6 -15
- package/report/generator/README.md +1 -1
- package/report/renderer/api.js +32 -0
- package/report/renderer/category-renderer.js +59 -19
- package/report/renderer/components.js +59 -110
- package/report/renderer/crc-details-renderer.js +15 -12
- package/report/renderer/dom.js +16 -1
- package/report/renderer/drop-down-menu.js +2 -2
- package/report/renderer/element-screenshot-renderer.js +8 -8
- package/report/renderer/features-util.js +1 -1
- package/report/renderer/open-tab.js +9 -3
- package/report/renderer/performance-category-renderer.js +55 -34
- package/report/renderer/pwa-category-renderer.js +0 -10
- package/report/renderer/report-renderer.js +91 -44
- package/report/renderer/report-ui-features.js +36 -33
- package/report/renderer/swap-locale-feature.js +3 -3
- package/report/renderer/topbar-features.js +53 -48
- package/report/renderer/util.js +66 -46
- package/report/test/clients/bundle-test.js +70 -0
- package/report/test/renderer/category-renderer-test.js +48 -3
- package/report/test/renderer/i18n-test.js +0 -7
- package/report/test/renderer/performance-category-renderer-test.js +51 -14
- package/report/test/renderer/pwa-category-renderer-test.js +6 -6
- package/report/test/renderer/report-renderer-axe-test.js +10 -12
- package/report/test/renderer/report-renderer-test.js +23 -22
- package/report/test/renderer/report-ui-features-test.js +20 -10
- package/report/test/renderer/text-encoding-test.js +1 -1
- package/report/test/renderer/util-test.js +35 -31
- package/report/test-assets/faux-psi-template.html +3 -11
- package/report/test-assets/faux-psi.js +26 -22
- package/report/types/html-renderer.d.ts +2 -0
- package/report/types/report-renderer.d.ts +23 -0
- package/shared/localization/format.js +64 -30
- package/shared/localization/locales/ar-XB.json +72 -75
- package/shared/localization/locales/ar.json +72 -75
- package/shared/localization/locales/bg.json +72 -75
- package/shared/localization/locales/ca.json +72 -75
- package/shared/localization/locales/cs.json +89 -92
- package/shared/localization/locales/da.json +72 -75
- package/shared/localization/locales/de.json +89 -92
- package/shared/localization/locales/el.json +72 -75
- package/shared/localization/locales/en-GB.json +72 -75
- package/shared/localization/locales/en-US.json +75 -75
- package/shared/localization/locales/en-XA.json +72 -75
- package/shared/localization/locales/en-XL.json +75 -75
- package/shared/localization/locales/es-419.json +90 -93
- package/shared/localization/locales/es.json +90 -93
- package/shared/localization/locales/fi.json +72 -75
- package/shared/localization/locales/fil.json +72 -75
- package/shared/localization/locales/fr.json +89 -92
- package/shared/localization/locales/he.json +73 -76
- package/shared/localization/locales/hi.json +72 -75
- package/shared/localization/locales/hr.json +72 -75
- package/shared/localization/locales/hu.json +89 -92
- package/shared/localization/locales/id.json +89 -92
- package/shared/localization/locales/it.json +89 -92
- package/shared/localization/locales/ja.json +89 -92
- package/shared/localization/locales/ko.json +89 -92
- package/shared/localization/locales/lt.json +89 -92
- package/shared/localization/locales/lv.json +95 -98
- package/shared/localization/locales/nl.json +89 -92
- package/shared/localization/locales/no.json +72 -75
- package/shared/localization/locales/pl.json +72 -75
- package/shared/localization/locales/pt-PT.json +72 -75
- package/shared/localization/locales/pt.json +89 -92
- package/shared/localization/locales/ro.json +90 -93
- package/shared/localization/locales/ru.json +89 -92
- package/shared/localization/locales/sk.json +89 -92
- package/shared/localization/locales/sl.json +89 -92
- package/shared/localization/locales/sr-Latn.json +89 -92
- package/shared/localization/locales/sr.json +89 -92
- package/shared/localization/locales/sv.json +72 -75
- package/shared/localization/locales/ta.json +89 -92
- package/shared/localization/locales/te.json +89 -92
- package/shared/localization/locales/th.json +89 -92
- package/shared/localization/locales/tr.json +89 -92
- package/shared/localization/locales/uk.json +92 -95
- package/shared/localization/locales/vi.json +89 -92
- package/shared/localization/locales/zh-HK.json +89 -92
- package/shared/localization/locales/zh-TW.json +89 -92
- package/shared/localization/locales/zh.json +89 -92
- package/shared/localization/locales.js +6 -0
- package/shared/test/localization/format-test.js +45 -9
- package/shared/test/localization/swap-locale-test.js +0 -12
- package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +19 -11
- package/third-party/chromium-synchronization/installability-errors-test.js +2 -0
- package/third-party/download-content-shell/download-content-shell.js +2 -2
- package/third-party/download-content-shell/utils.js +0 -24
- package/tsconfig-all.json +2 -2
- package/tsconfig.json +0 -1
- package/types/artifacts.d.ts +7 -7
- package/types/enquirer.d.ts +3 -1
- package/types/lhr/audit-details.d.ts +6 -5
- package/types/lhr/flow.d.ts +3 -2
- package/types/lhr/lhr.d.ts +1 -1
- package/types/node.d.ts +5 -5
- package/types/rollup-plugin-postprocess.d.ts +10 -0
- package/changelog.md +0 -5490
- package/lighthouse-core/audits/dobetterweb/appcache-manifest.js +0 -67
- package/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js +0 -96
- package/lighthouse-core/audits/redirects-http.js +0 -59
- package/lighthouse-core/gather/gatherers/dobetterweb/appcache.js +0 -34
- package/lighthouse-core/gather/gatherers/http-redirect.js +0 -46
- package/lighthouse-core/lib/i18n/locales/en-US.ctc.json +0 -6699
- package/lighthouse-core/lib/i18n/locales/en-XL.ctc.json +0 -6672
- package/lighthouse-core/scripts/package.json +0 -4
- package/report/clients/psi.js +0 -158
- package/report/test/clients/psi-test.js +0 -143
- package/shared/localization/locales/en-US.ctc.json +0 -6877
- package/shared/localization/locales/en-XL.ctc.json +0 -6852
|
@@ -37,7 +37,7 @@ const IGNORE_THRESHOLD_IN_BYTES = 2048;
|
|
|
37
37
|
const IGNORE_THRESHOLD_IN_PERCENT = 75;
|
|
38
38
|
const IGNORE_THRESHOLD_IN_MS = 50;
|
|
39
39
|
|
|
40
|
-
/** @typedef {{url: string, requestStartTime: number, totalBytes: number, wastedBytes: number, wastedPercent: number}} WasteResult */
|
|
40
|
+
/** @typedef {{node: LH.Audit.Details.NodeValue, url: string, requestStartTime: number, totalBytes: number, wastedBytes: number, wastedPercent: number}} WasteResult */
|
|
41
41
|
|
|
42
42
|
class OffscreenImages extends ByteEfficiencyAudit {
|
|
43
43
|
/**
|
|
@@ -100,6 +100,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
return {
|
|
103
|
+
node: ByteEfficiencyAudit.makeNodeItem(image.node),
|
|
103
104
|
url,
|
|
104
105
|
requestStartTime: networkRecord.startTime,
|
|
105
106
|
totalBytes,
|
|
@@ -184,26 +185,26 @@ class OffscreenImages extends ByteEfficiencyAudit {
|
|
|
184
185
|
|
|
185
186
|
/** @type {string[]} */
|
|
186
187
|
const warnings = [];
|
|
187
|
-
|
|
188
|
+
/** @type {Map<string, WasteResult>} */
|
|
189
|
+
const resultsMap = new Map();
|
|
190
|
+
for (const image of images) {
|
|
188
191
|
const processed = OffscreenImages.computeWaste(image, viewportDimensions, networkRecords);
|
|
189
192
|
if (processed === null) {
|
|
190
|
-
|
|
193
|
+
continue;
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
if (processed instanceof Error) {
|
|
194
197
|
warnings.push(processed.message);
|
|
195
198
|
Sentry.captureException(processed, {tags: {audit: this.meta.id}, level: 'warning'});
|
|
196
|
-
|
|
199
|
+
continue;
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
// If an image was used more than once, warn only about its least wasteful usage
|
|
200
|
-
const existing =
|
|
203
|
+
const existing = resultsMap.get(processed.url);
|
|
201
204
|
if (!existing || existing.wastedBytes > processed.wastedBytes) {
|
|
202
|
-
|
|
205
|
+
resultsMap.set(processed.url, processed);
|
|
203
206
|
}
|
|
204
|
-
|
|
205
|
-
return results;
|
|
206
|
-
}, /** @type {Map<string, WasteResult>} */ (new Map()));
|
|
207
|
+
}
|
|
207
208
|
|
|
208
209
|
const settings = context.settings;
|
|
209
210
|
|
|
@@ -233,7 +234,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
|
|
|
233
234
|
|
|
234
235
|
/** @type {LH.Audit.Details.Opportunity['headings']} */
|
|
235
236
|
const headings = [
|
|
236
|
-
{key: '
|
|
237
|
+
{key: 'node', valueType: 'node', label: ''},
|
|
237
238
|
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
|
|
238
239
|
{key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
|
|
239
240
|
{key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
|
|
@@ -76,10 +76,12 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
|
|
|
76
76
|
const imageElementsByURL = new Map();
|
|
77
77
|
imageElements.forEach(img => imageElementsByURL.set(img.src, img));
|
|
78
78
|
|
|
79
|
-
/** @type {Array<{url: string, fromProtocol: boolean, isCrossOrigin: boolean, totalBytes: number, wastedBytes: number}>} */
|
|
79
|
+
/** @type {Array<{node?: LH.Audit.Details.NodeValue, url: string, fromProtocol: boolean, isCrossOrigin: boolean, totalBytes: number, wastedBytes: number}>} */
|
|
80
80
|
const items = [];
|
|
81
81
|
const warnings = [];
|
|
82
82
|
for (const image of images) {
|
|
83
|
+
const imageElement = imageElementsByURL.get(image.url);
|
|
84
|
+
|
|
83
85
|
if (image.failed) {
|
|
84
86
|
warnings.push(`Unable to decode ${URL.getURLDisplayName(image.url)}`);
|
|
85
87
|
continue;
|
|
@@ -91,7 +93,6 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
|
|
|
91
93
|
let fromProtocol = true;
|
|
92
94
|
|
|
93
95
|
if (typeof jpegSize === 'undefined') {
|
|
94
|
-
const imageElement = imageElementsByURL.get(image.url);
|
|
95
96
|
if (!imageElement) {
|
|
96
97
|
warnings.push(`Unable to locate resource ${URL.getURLDisplayName(image.url)}`);
|
|
97
98
|
continue;
|
|
@@ -115,6 +116,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
|
|
|
115
116
|
const jpegSavings = UsesOptimizedImages.computeSavings({...image, jpegSize});
|
|
116
117
|
|
|
117
118
|
items.push({
|
|
119
|
+
node: imageElement ? ByteEfficiencyAudit.makeNodeItem(imageElement.node) : undefined,
|
|
118
120
|
url,
|
|
119
121
|
fromProtocol,
|
|
120
122
|
isCrossOrigin,
|
|
@@ -125,7 +127,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
|
|
|
125
127
|
|
|
126
128
|
/** @type {LH.Audit.Details.Opportunity['headings']} */
|
|
127
129
|
const headings = [
|
|
128
|
-
{key: '
|
|
130
|
+
{key: 'node', valueType: 'node', label: ''},
|
|
129
131
|
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
|
|
130
132
|
{key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
|
|
131
133
|
{key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
|
|
@@ -70,6 +70,7 @@ class UsesResponsiveImagesSnapshot extends Audit {
|
|
|
70
70
|
if (actualPixels - usedPixels > IGNORE_THRESHOLD_IN_PIXELS) score = 0;
|
|
71
71
|
|
|
72
72
|
items.push({
|
|
73
|
+
node: Audit.makeNodeItem(image.node),
|
|
73
74
|
url: URL.elideDataURI(image.src),
|
|
74
75
|
displayedDimensions: `${displayed.width}x${displayed.height}`,
|
|
75
76
|
actualDimensions: `${actual.width}x${actual.height}`,
|
|
@@ -79,7 +80,7 @@ class UsesResponsiveImagesSnapshot extends Audit {
|
|
|
79
80
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
80
81
|
const headings = [
|
|
81
82
|
/* eslint-disable max-len */
|
|
82
|
-
{key: '
|
|
83
|
+
{key: 'node', itemType: 'node', text: ''},
|
|
83
84
|
{key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
|
|
84
85
|
{key: 'displayedDimensions', itemType: 'text', text: str_(UIStrings.columnDisplayedDimensions)},
|
|
85
86
|
{key: 'actualDimensions', itemType: 'text', text: str_(UIStrings.columnActualDimensions)},
|
|
@@ -104,6 +104,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
|
|
|
104
104
|
const wastedBytes = Math.round(totalBytes * wastedRatio);
|
|
105
105
|
|
|
106
106
|
return {
|
|
107
|
+
node: ByteEfficiencyAudit.makeNodeItem(image.node),
|
|
107
108
|
url,
|
|
108
109
|
totalBytes,
|
|
109
110
|
wastedBytes,
|
|
@@ -158,7 +159,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
|
|
|
158
159
|
|
|
159
160
|
/** @type {LH.Audit.Details.Opportunity['headings']} */
|
|
160
161
|
const headings = [
|
|
161
|
-
{key: '
|
|
162
|
+
{key: 'node', valueType: 'node', label: ''},
|
|
162
163
|
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
|
|
163
164
|
{key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
|
|
164
165
|
{key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* that contain deprecated API warnings sent by Chrome.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
// TODO: when M97 is sufficiently old, drop support for console messages
|
|
15
|
+
|
|
14
16
|
const Audit = require('./audit.js');
|
|
15
17
|
const i18n = require('../lib/i18n/i18n.js');
|
|
16
18
|
|
|
@@ -45,7 +47,7 @@ class Deprecations extends Audit {
|
|
|
45
47
|
title: str_(UIStrings.title),
|
|
46
48
|
failureTitle: str_(UIStrings.failureTitle),
|
|
47
49
|
description: str_(UIStrings.description),
|
|
48
|
-
requiredArtifacts: ['ConsoleMessages'],
|
|
50
|
+
requiredArtifacts: ['ConsoleMessages', 'InspectorIssues'],
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -56,16 +58,36 @@ class Deprecations extends Audit {
|
|
|
56
58
|
static audit(artifacts) {
|
|
57
59
|
const entries = artifacts.ConsoleMessages;
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
let deprecations;
|
|
62
|
+
|
|
63
|
+
if (artifacts.InspectorIssues.deprecations.length) {
|
|
64
|
+
deprecations = artifacts.InspectorIssues.deprecations
|
|
65
|
+
.map(deprecation => {
|
|
66
|
+
return {
|
|
67
|
+
value: deprecation.message || '',
|
|
68
|
+
/** @type {LH.Audit.Details.SourceLocationValue} */
|
|
69
|
+
source: {
|
|
70
|
+
type: 'source-location',
|
|
71
|
+
url: deprecation.sourceCodeLocation.url,
|
|
72
|
+
urlProvider: 'network',
|
|
73
|
+
line: deprecation.sourceCodeLocation.lineNumber,
|
|
74
|
+
// Protocol.Audits.SourceCodeLocation.columnNumber is 1-indexed,
|
|
75
|
+
// but we use 0-indexed.
|
|
76
|
+
column: deprecation.sourceCodeLocation.columnNumber - 1,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
// Backcompat for <M97.
|
|
82
|
+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1248484
|
|
83
|
+
deprecations = entries.filter(log => log.source === 'deprecation')
|
|
84
|
+
.map(log => {
|
|
85
|
+
return {
|
|
86
|
+
value: log.text,
|
|
87
|
+
source: Audit.makeSourceLocationFromConsoleMessage(log),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
69
91
|
|
|
70
92
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
71
93
|
const headings = [
|
|
@@ -61,8 +61,8 @@ class UsesHTTP2Audit extends Audit {
|
|
|
61
61
|
title: str_(UIStrings.title),
|
|
62
62
|
description: str_(UIStrings.description),
|
|
63
63
|
scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
|
|
64
|
-
supportedModes: ['navigation'],
|
|
65
|
-
requiredArtifacts: ['URL', 'devtoolsLogs', 'traces'],
|
|
64
|
+
supportedModes: ['timespan', 'navigation'],
|
|
65
|
+
requiredArtifacts: ['URL', 'devtoolsLogs', 'traces', 'GatherContext'],
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -202,24 +202,40 @@ class UsesHTTP2Audit extends Audit {
|
|
|
202
202
|
static async audit(artifacts, context) {
|
|
203
203
|
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
204
204
|
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
205
|
+
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
|
|
206
|
+
const resources = UsesHTTP2Audit.determineNonHttp2Resources(networkRecords);
|
|
207
|
+
|
|
208
|
+
let displayValue;
|
|
209
|
+
if (resources.length > 0) {
|
|
210
|
+
displayValue = str_(UIStrings.displayValue, {itemCount: resources.length});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// TODO: Compute actual savings for timespan mode.
|
|
214
|
+
if (artifacts.GatherContext.gatherMode === 'timespan') {
|
|
215
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
216
|
+
const headings = [
|
|
217
|
+
{key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
|
|
218
|
+
{key: 'protocol', itemType: 'text', text: str_(UIStrings.columnProtocol)},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const details = Audit.makeTableDetails(headings, resources);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
displayValue,
|
|
225
|
+
score: resources.length ? 0 : 1,
|
|
226
|
+
details,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
205
230
|
const settings = context && context.settings || {};
|
|
206
231
|
const simulatorOptions = {
|
|
207
232
|
devtoolsLog,
|
|
208
233
|
settings,
|
|
209
234
|
};
|
|
210
|
-
|
|
211
|
-
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
|
|
212
235
|
const graph = await PageDependencyGraph.request({trace, devtoolsLog}, context);
|
|
213
236
|
const simulator = await LoadSimulator.request(simulatorOptions, context);
|
|
214
|
-
|
|
215
|
-
const resources = UsesHTTP2Audit.determineNonHttp2Resources(networkRecords);
|
|
216
237
|
const wastedMs = UsesHTTP2Audit.computeWasteWithTTIGraph(resources, graph, simulator);
|
|
217
238
|
|
|
218
|
-
let displayValue;
|
|
219
|
-
if (resources.length > 0) {
|
|
220
|
-
displayValue = str_(UIStrings.displayValue, {itemCount: resources.length});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
239
|
/** @type {LH.Audit.Details.Opportunity['headings']} */
|
|
224
240
|
const headings = [
|
|
225
241
|
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
|
|
@@ -51,7 +51,7 @@ class ImageAspectRatio extends Audit {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* @param {WellDefinedImage} image
|
|
54
|
-
* @return {{url: string, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}}
|
|
54
|
+
* @return {{url: string, node: LH.Audit.Details.NodeValue, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}}
|
|
55
55
|
*/
|
|
56
56
|
static computeAspectRatios(image) {
|
|
57
57
|
const url = URL.elideDataURI(image.src);
|
|
@@ -63,6 +63,7 @@ class ImageAspectRatio extends Audit {
|
|
|
63
63
|
|
|
64
64
|
return {
|
|
65
65
|
url,
|
|
66
|
+
node: Audit.makeNodeItem(image.node),
|
|
66
67
|
displayedAspectRatio: `${image.displayedWidth} x ${image.displayedHeight}
|
|
67
68
|
(${displayedAspectRatio.toFixed(2)})`,
|
|
68
69
|
actualAspectRatio: `${image.naturalDimensions.width} x ${image.naturalDimensions.height}
|
|
@@ -78,7 +79,7 @@ class ImageAspectRatio extends Audit {
|
|
|
78
79
|
static audit(artifacts) {
|
|
79
80
|
const images = artifacts.ImageElements;
|
|
80
81
|
|
|
81
|
-
/** @type {Array<{url: string, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}>} */
|
|
82
|
+
/** @type {Array<{url: string, node: LH.Audit.Details.NodeValue, displayedAspectRatio: string, actualAspectRatio: string, doRatiosMatch: boolean}>} */
|
|
82
83
|
const results = [];
|
|
83
84
|
images.filter(image => {
|
|
84
85
|
// - filter out css background images since we don't have a reliable way to tell if it's a
|
|
@@ -88,8 +89,7 @@ class ImageAspectRatio extends Audit {
|
|
|
88
89
|
// - filter all svgs as they have no natural dimensions to audit
|
|
89
90
|
// - filter out images that have falsy naturalWidth or naturalHeight
|
|
90
91
|
return !image.isCss &&
|
|
91
|
-
image.
|
|
92
|
-
image.mimeType !== 'image/svg+xml' &&
|
|
92
|
+
URL.guessMimeType(image.src) !== 'image/svg+xml' &&
|
|
93
93
|
image.naturalDimensions &&
|
|
94
94
|
image.naturalDimensions.height > 5 &&
|
|
95
95
|
image.naturalDimensions.width > 5 &&
|
|
@@ -105,7 +105,7 @@ class ImageAspectRatio extends Audit {
|
|
|
105
105
|
|
|
106
106
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
107
107
|
const headings = [
|
|
108
|
-
{key: '
|
|
108
|
+
{key: 'node', itemType: 'node', text: ''},
|
|
109
109
|
{key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
|
|
110
110
|
{key: 'displayedAspectRatio', itemType: 'text', text: str_(UIStrings.columnDisplayed)},
|
|
111
111
|
{key: 'actualAspectRatio', itemType: 'text', text: str_(UIStrings.columnActual)},
|
|
@@ -45,7 +45,7 @@ const LARGE_IMAGE_FACTOR = 0.75;
|
|
|
45
45
|
// considered SMALL.
|
|
46
46
|
const SMALL_IMAGE_THRESHOLD = 64;
|
|
47
47
|
|
|
48
|
-
/** @typedef {{url: string,
|
|
48
|
+
/** @typedef {{url: string, node: LH.Audit.Details.NodeValue, displayedSize: string, actualSize: string, actualPixels: number, expectedSize: string, expectedPixels: number}} Result */
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* @param {{top: number, bottom: number, left: number, right: number}} imageRect
|
|
@@ -95,7 +95,7 @@ function isCandidate(image) {
|
|
|
95
95
|
) {
|
|
96
96
|
return false;
|
|
97
97
|
}
|
|
98
|
-
if (image.
|
|
98
|
+
if (URL.guessMimeType(image.src) === 'image/svg+xml') {
|
|
99
99
|
return false;
|
|
100
100
|
}
|
|
101
101
|
if (image.isCss) {
|
|
@@ -146,8 +146,8 @@ function getResult(image, DPR) {
|
|
|
146
146
|
const [expectedWidth, expectedHeight] =
|
|
147
147
|
expectedImageSize(image.displayedWidth, image.displayedHeight, DPR);
|
|
148
148
|
return {
|
|
149
|
-
url: image.src,
|
|
150
|
-
|
|
149
|
+
url: URL.elideDataURI(image.src),
|
|
150
|
+
node: Audit.makeNodeItem(image.node),
|
|
151
151
|
displayedSize: `${image.displayedWidth} x ${image.displayedHeight}`,
|
|
152
152
|
actualSize: `${image.naturalDimensions.width} x ${image.naturalDimensions.height}`,
|
|
153
153
|
actualPixels: image.naturalDimensions.width * image.naturalDimensions.height,
|
|
@@ -263,8 +263,8 @@ class ImageSizeResponsive extends Audit {
|
|
|
263
263
|
|
|
264
264
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
265
265
|
const headings = [
|
|
266
|
-
{key: '
|
|
267
|
-
{key: '
|
|
266
|
+
{key: 'node', itemType: 'node', text: ''},
|
|
267
|
+
{key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
|
|
268
268
|
{key: 'displayedSize', itemType: 'text', text: str_(UIStrings.columnDisplayed)},
|
|
269
269
|
{key: 'actualSize', itemType: 'text', text: str_(UIStrings.columnActual)},
|
|
270
270
|
{key: 'expectedSize', itemType: 'text', text: str_(UIStrings.columnExpected)},
|
|
@@ -50,9 +50,9 @@ const UIStrings = {
|
|
|
50
50
|
* for the current page encloses the scope and start URL from the manifest. */
|
|
51
51
|
'no-matching-service-worker': `No matching service worker detected. You may need to reload the page, or check that the scope of the service worker for the current page encloses the scope and start URL from the manifest.`,
|
|
52
52
|
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
* @description Error message explaining that the manifest does not contain a suitable icon.
|
|
54
|
+
* @example {192} value0
|
|
55
|
+
*/
|
|
56
56
|
'manifest-missing-suitable-icon': `Manifest does not contain a suitable icon - PNG, SVG or WebP format of at least {value0}\xa0px is required, the sizes attribute must be set, and the purpose attribute, if set, must include "any".`,
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -93,6 +93,8 @@ const UIStrings = {
|
|
|
93
93
|
'manifest-location-changed': `Manifest URL changed while the manifest was being fetched.`,
|
|
94
94
|
/** Warning message explaining that the page does not work offline. */
|
|
95
95
|
'warn-not-offline-capable': `Page does not work offline. The page will not be regarded as installable after Chrome 93, stable release August 2021.`,
|
|
96
|
+
/** Error message explaining that Lighthouse failed while detecting a service worker, and directing the user to try again in a new Chrome. */
|
|
97
|
+
'protocol-timeout': `Lighthouse could not determine if there was a service worker. Please try with a newer version of Chrome.`,
|
|
96
98
|
};
|
|
97
99
|
/* eslint-enable max-len */
|
|
98
100
|
|
|
@@ -106,13 +106,14 @@ class PreloadLCPImageAudit extends Audit {
|
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
108
|
* Computes the estimated effect of preloading the LCP image.
|
|
109
|
+
* @param {LH.Artifacts.TraceElement|undefined} lcpElement
|
|
109
110
|
* @param {LH.Gatherer.Simulation.GraphNetworkNode|undefined} lcpNode
|
|
110
111
|
* @param {LH.Gatherer.Simulation.GraphNode} graph
|
|
111
112
|
* @param {LH.Gatherer.Simulation.Simulator} simulator
|
|
112
|
-
* @return {{wastedMs: number, results: Array<{url: string, wastedMs: number}>}}
|
|
113
|
+
* @return {{wastedMs: number, results: Array<{node: LH.Audit.Details.NodeValue, url: string, wastedMs: number}>}}
|
|
113
114
|
*/
|
|
114
|
-
static computeWasteWithGraph(lcpNode, graph, simulator) {
|
|
115
|
-
if (!lcpNode) {
|
|
115
|
+
static computeWasteWithGraph(lcpElement, lcpNode, graph, simulator) {
|
|
116
|
+
if (!lcpElement || !lcpNode) {
|
|
116
117
|
return {
|
|
117
118
|
wastedMs: 0,
|
|
118
119
|
results: [],
|
|
@@ -185,6 +186,7 @@ class PreloadLCPImageAudit extends Audit {
|
|
|
185
186
|
return {
|
|
186
187
|
wastedMs,
|
|
187
188
|
results: [{
|
|
189
|
+
node: Audit.makeNodeItem(lcpElement.node),
|
|
188
190
|
url: lcpNode.record.url,
|
|
189
191
|
wastedMs,
|
|
190
192
|
}],
|
|
@@ -216,11 +218,11 @@ class PreloadLCPImageAudit extends Audit {
|
|
|
216
218
|
const lcpNode = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
|
|
217
219
|
|
|
218
220
|
const {results, wastedMs} =
|
|
219
|
-
PreloadLCPImageAudit.computeWasteWithGraph(lcpNode, graph, simulator);
|
|
221
|
+
PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNode, graph, simulator);
|
|
220
222
|
|
|
221
223
|
/** @type {LH.Audit.Details.Opportunity['headings']} */
|
|
222
224
|
const headings = [
|
|
223
|
-
{key: '
|
|
225
|
+
{key: 'node', valueType: 'node', label: ''},
|
|
224
226
|
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
|
|
225
227
|
{key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
|
|
226
228
|
];
|
|
@@ -106,7 +106,7 @@ class UnsizedImages extends Audit {
|
|
|
106
106
|
* @return {boolean}
|
|
107
107
|
*/
|
|
108
108
|
static isNonNetworkSvg(image) {
|
|
109
|
-
const isSvg = image.
|
|
109
|
+
const isSvg = URL.guessMimeType(image.src) === 'image/svg+xml';
|
|
110
110
|
const urlScheme = image.src.slice(0, image.src.indexOf(':'));
|
|
111
111
|
const isNonNetwork = URL.isNonNetworkProtocol(urlScheme);
|
|
112
112
|
return isSvg && isNonNetwork;
|
|
@@ -147,9 +147,8 @@ class UnsizedImages extends Audit {
|
|
|
147
147
|
|
|
148
148
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
149
149
|
const headings = [
|
|
150
|
-
{key: '
|
|
150
|
+
{key: 'node', itemType: 'node', text: ''},
|
|
151
151
|
{key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)},
|
|
152
|
-
{key: 'node', itemType: 'node', text: str_(i18n.UIStrings.columnFailingElem)},
|
|
153
152
|
];
|
|
154
153
|
|
|
155
154
|
return {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const URL = require('../lib/url-shim.js');
|
|
8
9
|
const makeComputedArtifact = require('./computed-artifact.js');
|
|
9
10
|
|
|
10
11
|
class ImageRecords {
|
|
@@ -28,12 +29,12 @@ class ImageRecords {
|
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* @param {{ImageElements: LH.Artifacts['ImageElements'], networkRecords: LH.Artifacts.NetworkRequest[]}} data
|
|
31
|
-
* @return {Promise<LH.Artifacts.
|
|
32
|
+
* @return {Promise<LH.Artifacts.ImageElementRecord[]>}
|
|
32
33
|
*/
|
|
33
34
|
static async compute_(data) {
|
|
34
35
|
const indexedNetworkRecords = ImageRecords.indexNetworkRecords(data.networkRecords);
|
|
35
36
|
|
|
36
|
-
/** @type {LH.Artifacts.
|
|
37
|
+
/** @type {LH.Artifacts.ImageElementRecord[]} */
|
|
37
38
|
const imageRecords = [];
|
|
38
39
|
|
|
39
40
|
for (const element of data.ImageElements) {
|
|
@@ -43,7 +44,7 @@ class ImageRecords {
|
|
|
43
44
|
// Don't change the guessed mime type if no mime type was found.
|
|
44
45
|
imageRecords.push({
|
|
45
46
|
...element,
|
|
46
|
-
mimeType: mimeType ? mimeType : element.
|
|
47
|
+
mimeType: mimeType ? mimeType : URL.guessMimeType(element.src),
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -10,6 +10,7 @@ const LanternMetric = require('./lantern-metric.js');
|
|
|
10
10
|
const BaseNode = require('../../lib/dependency-graph/base-node.js');
|
|
11
11
|
const LanternFirstContentfulPaint = require('./lantern-first-contentful-paint.js');
|
|
12
12
|
const LanternInteractive = require('./lantern-interactive.js');
|
|
13
|
+
const {BLOCKING_TIME_THRESHOLD, calculateSumOfBlockingTime} = require('./tbt-utils.js');
|
|
13
14
|
|
|
14
15
|
/** @typedef {BaseNode.Node} Node */
|
|
15
16
|
|
|
@@ -66,9 +67,7 @@ class LanternTotalBlockingTime extends LanternMetric {
|
|
|
66
67
|
? extras.interactiveResult.optimisticEstimate.timeInMs
|
|
67
68
|
: extras.interactiveResult.pessimisticEstimate.timeInMs;
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
const TotalBlockingTime = require('./total-blocking-time.js');
|
|
71
|
-
const minDurationMs = TotalBlockingTime.BLOCKING_TIME_THRESHOLD;
|
|
70
|
+
const minDurationMs = BLOCKING_TIME_THRESHOLD;
|
|
72
71
|
|
|
73
72
|
const events = LanternTotalBlockingTime.getTopLevelEvents(
|
|
74
73
|
simulation.nodeTimings,
|
|
@@ -76,7 +75,7 @@ class LanternTotalBlockingTime extends LanternMetric {
|
|
|
76
75
|
);
|
|
77
76
|
|
|
78
77
|
return {
|
|
79
|
-
timeInMs:
|
|
78
|
+
timeInMs: calculateSumOfBlockingTime(
|
|
80
79
|
events,
|
|
81
80
|
fcpTimeInMs,
|
|
82
81
|
interactiveTimeMs
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
|
|
3
|
+
* 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
|
|
4
|
+
* 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.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const BLOCKING_TIME_THRESHOLD = 50;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Array<{start: number, end: number, duration: number}>} topLevelEvents
|
|
12
|
+
* @param {number} startTimeMs
|
|
13
|
+
* @param {number} endTimeMs
|
|
14
|
+
* @return {number}
|
|
15
|
+
*/
|
|
16
|
+
function calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
|
|
17
|
+
if (endTimeMs <= startTimeMs) return 0;
|
|
18
|
+
|
|
19
|
+
const threshold = BLOCKING_TIME_THRESHOLD;
|
|
20
|
+
let sumBlockingTime = 0;
|
|
21
|
+
for (const event of topLevelEvents) {
|
|
22
|
+
// Early exit for small tasks, which should far outnumber long tasks.
|
|
23
|
+
if (event.duration < threshold) continue;
|
|
24
|
+
|
|
25
|
+
// We only want to consider tasks that fall in our time range (FCP and TTI for navigations).
|
|
26
|
+
// FCP is picked as the lower bound because there is little risk of user input happening
|
|
27
|
+
// before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
|
|
28
|
+
// optimizing to reach FCP as fast as possible without having to worry about task lengths.
|
|
29
|
+
if (event.end < startTimeMs) continue;
|
|
30
|
+
|
|
31
|
+
// TTI is picked as the upper bound because we want a well defined end point for page load.
|
|
32
|
+
if (event.start > endTimeMs) continue;
|
|
33
|
+
|
|
34
|
+
// We first perform the clipping, and then calculate Blocking Region. So if we have a 150ms
|
|
35
|
+
// task [0, 150] and FCP happens midway at 50ms, we first clip the task to [50, 150], and then
|
|
36
|
+
// calculate the Blocking Region to be [100, 150]. The rational here is that tasks before FCP
|
|
37
|
+
// are unimportant, so we care whether the main thread is busy more than 50ms at a time only
|
|
38
|
+
// after FCP.
|
|
39
|
+
const clippedStart = Math.max(event.start, startTimeMs);
|
|
40
|
+
const clippedEnd = Math.min(event.end, endTimeMs);
|
|
41
|
+
const clippedDuration = clippedEnd - clippedStart;
|
|
42
|
+
if (clippedDuration < threshold) continue;
|
|
43
|
+
|
|
44
|
+
// The duration of the task beyond 50ms at the beginning is considered the Blocking Region.
|
|
45
|
+
// Example:
|
|
46
|
+
// [ 250ms Task ]
|
|
47
|
+
// | First 50ms | Blocking Region (200ms) |
|
|
48
|
+
sumBlockingTime += clippedDuration - threshold;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return sumBlockingTime;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
BLOCKING_TIME_THRESHOLD,
|
|
56
|
+
calculateSumOfBlockingTime,
|
|
57
|
+
};
|
|
@@ -10,6 +10,7 @@ const ComputedMetric = require('./metric.js');
|
|
|
10
10
|
const TraceProcessor = require('../../lib/tracehouse/trace-processor.js');
|
|
11
11
|
const LanternTotalBlockingTime = require('./lantern-total-blocking-time.js');
|
|
12
12
|
const TimetoInteractive = require('./interactive.js');
|
|
13
|
+
const {calculateSumOfBlockingTime} = require('./tbt-utils.js');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @fileoverview This audit determines Total Blocking Time.
|
|
@@ -25,57 +26,6 @@ const TimetoInteractive = require('./interactive.js');
|
|
|
25
26
|
* to smaller improvements to main thread responsiveness.
|
|
26
27
|
*/
|
|
27
28
|
class TotalBlockingTime extends ComputedMetric {
|
|
28
|
-
/**
|
|
29
|
-
* @return {number}
|
|
30
|
-
*/
|
|
31
|
-
static get BLOCKING_TIME_THRESHOLD() {
|
|
32
|
-
return 50;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @param {Array<{start: number, end: number, duration: number}>} topLevelEvents
|
|
37
|
-
* @param {number} startTimeMs
|
|
38
|
-
* @param {number} endTimeMs
|
|
39
|
-
* @return {number}
|
|
40
|
-
*/
|
|
41
|
-
static calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
|
|
42
|
-
if (endTimeMs <= startTimeMs) return 0;
|
|
43
|
-
|
|
44
|
-
const threshold = TotalBlockingTime.BLOCKING_TIME_THRESHOLD;
|
|
45
|
-
let sumBlockingTime = 0;
|
|
46
|
-
for (const event of topLevelEvents) {
|
|
47
|
-
// Early exit for small tasks, which should far outnumber long tasks.
|
|
48
|
-
if (event.duration < threshold) continue;
|
|
49
|
-
|
|
50
|
-
// We only want to consider tasks that fall in our time range (FCP and TTI for navigations).
|
|
51
|
-
// FCP is picked as the lower bound because there is little risk of user input happening
|
|
52
|
-
// before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
|
|
53
|
-
// optimizing to reach FCP as fast as possible without having to worry about task lengths.
|
|
54
|
-
if (event.end < startTimeMs) continue;
|
|
55
|
-
|
|
56
|
-
// TTI is picked as the upper bound because we want a well defined end point for page load.
|
|
57
|
-
if (event.start > endTimeMs) continue;
|
|
58
|
-
|
|
59
|
-
// We first perform the clipping, and then calculate Blocking Region. So if we have a 150ms
|
|
60
|
-
// task [0, 150] and FCP happens midway at 50ms, we first clip the task to [50, 150], and then
|
|
61
|
-
// calculate the Blocking Region to be [100, 150]. The rational here is that tasks before FCP
|
|
62
|
-
// are unimportant, so we care whether the main thread is busy more than 50ms at a time only
|
|
63
|
-
// after FCP.
|
|
64
|
-
const clippedStart = Math.max(event.start, startTimeMs);
|
|
65
|
-
const clippedEnd = Math.min(event.end, endTimeMs);
|
|
66
|
-
const clippedDuration = clippedEnd - clippedStart;
|
|
67
|
-
if (clippedDuration < threshold) continue;
|
|
68
|
-
|
|
69
|
-
// The duration of the task beyond 50ms at the beginning is considered the Blocking Region.
|
|
70
|
-
// Example:
|
|
71
|
-
// [ 250ms Task ]
|
|
72
|
-
// | First 50ms | Blocking Region (200ms) |
|
|
73
|
-
sumBlockingTime += clippedDuration - threshold;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return sumBlockingTime;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
29
|
/**
|
|
80
30
|
* @param {LH.Artifacts.MetricComputationData} data
|
|
81
31
|
* @param {LH.Artifacts.ComputedContext} context
|
|
@@ -100,7 +50,7 @@ class TotalBlockingTime extends ComputedMetric {
|
|
|
100
50
|
const interactiveTimeMs = (await TimetoInteractive.request(metricData, context)).timing;
|
|
101
51
|
|
|
102
52
|
return {
|
|
103
|
-
timing:
|
|
53
|
+
timing: calculateSumOfBlockingTime(
|
|
104
54
|
events,
|
|
105
55
|
firstContentfulPaint,
|
|
106
56
|
interactiveTimeMs
|
|
@@ -108,7 +58,7 @@ class TotalBlockingTime extends ComputedMetric {
|
|
|
108
58
|
};
|
|
109
59
|
} else {
|
|
110
60
|
return {
|
|
111
|
-
timing:
|
|
61
|
+
timing: calculateSumOfBlockingTime(
|
|
112
62
|
events,
|
|
113
63
|
0,
|
|
114
64
|
data.processedTrace.timestamps.traceEnd
|