lighthouse 9.5.0-dev.20220321 → 9.5.0-dev.20220322

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.
@@ -163,14 +163,13 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
163
163
  /** @type {number|undefined} */
164
164
  let transferRatio = transferRatioByUrl.get(url);
165
165
  if (transferRatio === undefined) {
166
- const networkRecord = getRequestForScript(networkRecords, script);
167
-
168
166
  if (!script || script.length === undefined) {
169
167
  // This should never happen because we found the wasted bytes from bundles, which required contents in a ScriptElement.
170
168
  continue;
171
169
  }
172
170
 
173
171
  const contentLength = script.length;
172
+ const networkRecord = getRequestForScript(networkRecords, script);
174
173
  transferRatio = DuplicatedJavascript._estimateTransferRatio(networkRecord, contentLength);
175
174
  transferRatioByUrl.set(url, transferRatio);
176
175
  }
@@ -373,12 +373,12 @@ class LegacyJavascript extends ByteEfficiencyAudit {
373
373
  if (transferRatio !== undefined) return transferRatio;
374
374
 
375
375
  const script = artifacts.Scripts.find(script => script.url === url);
376
- const networkRecord = getRequestForScript(networkRecords, script);
377
376
 
378
377
  if (!script || script.content === null) {
379
378
  // Can't find content, so just use 1.
380
379
  transferRatio = 1;
381
380
  } else {
381
+ const networkRecord = getRequestForScript(networkRecords, script);
382
382
  const contentLength = script.length || 0;
383
383
  const transferSize =
384
384
  ByteEfficiencyAudit.estimateTransferSize(networkRecord, contentLength, 'Script');
@@ -9,7 +9,7 @@ const Audit = require('./audit.js');
9
9
  const MainThreadTasksComputed = require('../computed/main-thread-tasks.js');
10
10
  const NetworkRecordsComputed = require('../computed/network-records.js');
11
11
  const NetworkAnalysisComputed = require('../computed/network-analysis.js');
12
- const NetworkAnalyzer = require('../lib/dependency-graph/simulator/network-analyzer.js');
12
+ const MainResource = require('../computed/main-resource.js');
13
13
 
14
14
  class Diagnostics extends Audit {
15
15
  /**
@@ -22,7 +22,7 @@ class Diagnostics extends Audit {
22
22
  title: 'Diagnostics',
23
23
  description: 'Collection of useful page vitals.',
24
24
  supportedModes: ['navigation'],
25
- requiredArtifacts: ['traces', 'devtoolsLogs'],
25
+ requiredArtifacts: ['URL', 'traces', 'devtoolsLogs'],
26
26
  };
27
27
  }
28
28
 
@@ -37,9 +37,10 @@ class Diagnostics extends Audit {
37
37
  const tasks = await MainThreadTasksComputed.request(trace, context);
38
38
  const records = await NetworkRecordsComputed.request(devtoolsLog, context);
39
39
  const analysis = await NetworkAnalysisComputed.request(devtoolsLog, context);
40
+ const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
40
41
 
41
42
  const toplevelTasks = tasks.filter(t => !t.parent);
42
- const mainDocumentTransferSize = NetworkAnalyzer.findMainDocument(records).transferSize;
43
+ const mainDocumentTransferSize = mainResource.transferSize;
43
44
  const totalByteWeight = records.reduce((sum, r) => sum + (r.transferSize || 0), 0);
44
45
  const totalTaskTime = toplevelTasks.reduce((sum, t) => sum + (t.duration || 0), 0);
45
46
  const maxRtt = Math.max(...analysis.additionalRttByOrigin.values()) + analysis.rtt;
@@ -6,11 +6,10 @@
6
6
  'use strict';
7
7
 
8
8
  const Audit = require('../audit.js');
9
- const NetworkRecords = require('../../computed/network-records.js');
10
9
  const HTTP_UNSUCCESSFUL_CODE_LOW = 400;
11
10
  const HTTP_UNSUCCESSFUL_CODE_HIGH = 599;
12
11
  const i18n = require('../../lib/i18n/i18n.js');
13
- const NetworkAnalyzer = require('../../lib/dependency-graph/simulator/network-analyzer.js');
12
+ const MainResource = require('../../computed/main-resource.js');
14
13
 
15
14
  const UIStrings = {
16
15
  /** Title of a Lighthouse audit that provides detail on the HTTP status code a page responds with. This descriptive title is shown when the page has responded with a valid HTTP status code. */
@@ -47,8 +46,7 @@ class HTTPStatusCode extends Audit {
47
46
  static async audit(artifacts, context) {
48
47
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
49
48
  const URL = artifacts.URL;
50
- const networkRecords = await NetworkRecords.request(devtoolsLog, context);
51
- const mainResource = NetworkAnalyzer.findMainDocument(networkRecords, URL.finalUrl);
49
+ const mainResource = await MainResource.request({devtoolsLog, URL}, context);
52
50
 
53
51
  const statusCode = mainResource.statusCode;
54
52
 
@@ -8,8 +8,6 @@
8
8
  const Audit = require('./audit.js');
9
9
  const i18n = require('../lib/i18n/i18n.js');
10
10
  const MainResource = require('../computed/main-resource.js');
11
- const NetworkRecords = require('../computed/network-records.js');
12
- const NetworkAnalyzer = require('../lib/dependency-graph/simulator/network-analyzer.js');
13
11
 
14
12
  const UIStrings = {
15
13
  /** Title of a diagnostic audit that provides detail on how long it took from starting a request to when the server started responding. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
@@ -39,7 +37,7 @@ class ServerResponseTime extends Audit {
39
37
  title: str_(UIStrings.title),
40
38
  failureTitle: str_(UIStrings.failureTitle),
41
39
  description: str_(UIStrings.description),
42
- supportedModes: ['timespan', 'navigation'],
40
+ supportedModes: ['navigation'],
43
41
  requiredArtifacts: ['devtoolsLogs', 'URL', 'GatherContext'],
44
42
  };
45
43
  }
@@ -61,20 +59,7 @@ class ServerResponseTime extends Audit {
61
59
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
62
60
 
63
61
  /** @type {LH.Artifacts.NetworkRequest} */
64
- let mainResource;
65
- if (artifacts.GatherContext.gatherMode === 'timespan') {
66
- const networkRecords = await NetworkRecords.request(devtoolsLog, context);
67
- const optionalMainResource = NetworkAnalyzer.findOptionalMainDocument(
68
- networkRecords,
69
- artifacts.URL.finalUrl
70
- );
71
- if (!optionalMainResource) {
72
- return {score: null, notApplicable: true};
73
- }
74
- mainResource = optionalMainResource;
75
- } else {
76
- mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
77
- }
62
+ const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
78
63
 
79
64
  const responseTime = ServerResponseTime.calculateResponseTime(mainResource);
80
65
  const passed = responseTime < TOO_SLOW_THRESHOLD_MS;
@@ -394,6 +394,9 @@ class PageDependencyGraph {
394
394
  const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRecords);
395
395
  const cpuNodes = PageDependencyGraph.getCPUNodes(processedTrace);
396
396
 
397
+ // TODO: Remove this usage of `findMainDocument` and create a new function to get the first document request.
398
+ // https://github.com/GoogleChrome/lighthouse/issues/8984
399
+ //
397
400
  // The main document request is the earliest network request *of type document*.
398
401
  // This will be different from the root request when there are server redirects.
399
402
  const mainDocumentRequest = NetworkAnalyzer.findMainDocument(networkRecords);
@@ -224,6 +224,7 @@ async function _navigation(navigationContext) {
224
224
  await collectPhaseArtifacts({phase: 'startInstrumentation', ...phaseState});
225
225
  await collectPhaseArtifacts({phase: 'startSensitiveInstrumentation', ...phaseState});
226
226
  const navigateResult = await _navigate(navigationContext);
227
+ phaseState.baseArtifacts.URL.finalUrl = navigateResult.finalUrl;
227
228
  phaseState.url = navigateResult.finalUrl;
228
229
  await collectPhaseArtifacts({phase: 'stopSensitiveInstrumentation', ...phaseState});
229
230
  await collectPhaseArtifacts({phase: 'stopInstrumentation', ...phaseState});
@@ -80,6 +80,7 @@ class GatherRunner {
80
80
  ...passContext.passConfig,
81
81
  });
82
82
  passContext.url = finalUrl;
83
+ passContext.baseArtifacts.URL.finalUrl = finalUrl;
83
84
  if (passContext.passConfig.loadFailureMode === 'fatal') {
84
85
  passContext.LighthouseRunWarnings.push(...warnings);
85
86
  }
@@ -8,10 +8,9 @@
8
8
  const LinkHeader = require('http-link-header');
9
9
  const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
10
10
  const {URL} = require('../../lib/url-shim.js');
11
- const NetworkRecords = require('../../computed/network-records.js');
12
- const NetworkAnalyzer = require('../../lib/dependency-graph/simulator/network-analyzer.js');
13
11
  const pageFunctions = require('../../lib/page-functions.js');
14
12
  const DevtoolsLog = require('./devtools-log.js');
13
+ const MainResource = require('../../computed/main-resource.js');
15
14
 
16
15
  /* globals HTMLLinkElement getNodeDetails */
17
16
 
@@ -114,12 +113,12 @@ class LinkElements extends FRGatherer {
114
113
 
115
114
  /**
116
115
  * @param {LH.Gatherer.FRTransitionalContext} context
117
- * @param {LH.Artifacts.NetworkRequest[]} networkRecords
118
- * @return {LH.Artifacts['LinkElements']}
116
+ * @param {LH.Artifacts['DevtoolsLog']} devtoolsLog
117
+ * @return {Promise<LH.Artifacts['LinkElements']>}
119
118
  */
120
- static getLinkElementsInHeaders(context, networkRecords) {
121
- const finalUrl = context.url;
122
- const mainDocument = NetworkAnalyzer.findMainDocument(networkRecords, finalUrl);
119
+ static async getLinkElementsInHeaders(context, devtoolsLog) {
120
+ const mainDocument =
121
+ await MainResource.request({devtoolsLog, URL: context.baseArtifacts.URL}, context);
123
122
 
124
123
  /** @type {LH.Artifacts['LinkElements']} */
125
124
  const linkElements = [];
@@ -130,7 +129,7 @@ class LinkElements extends FRGatherer {
130
129
  for (const link of LinkHeader.parse(header.value).refs) {
131
130
  linkElements.push({
132
131
  rel: link.rel || '',
133
- href: normalizeUrlOrNull(link.uri, finalUrl),
132
+ href: normalizeUrlOrNull(link.uri, context.baseArtifacts.URL.finalUrl),
134
133
  hrefRaw: link.uri || '',
135
134
  hreflang: link.hreflang || '',
136
135
  as: link.as || '',
@@ -146,12 +145,12 @@ class LinkElements extends FRGatherer {
146
145
 
147
146
  /**
148
147
  * @param {LH.Gatherer.FRTransitionalContext} context
149
- * @param {LH.Artifacts.NetworkRequest[]} networkRecords
148
+ * @param {LH.Artifacts['DevtoolsLog']} devtoolsLog
150
149
  * @return {Promise<LH.Artifacts['LinkElements']>}
151
150
  */
152
- async _getArtifact(context, networkRecords) {
151
+ async _getArtifact(context, devtoolsLog) {
153
152
  const fromDOM = await LinkElements.getLinkElementsInDOM(context);
154
- const fromHeaders = LinkElements.getLinkElementsInHeaders(context, networkRecords);
153
+ const fromHeaders = await LinkElements.getLinkElementsInHeaders(context, devtoolsLog);
155
154
  const linkElements = fromDOM.concat(fromHeaders);
156
155
 
157
156
  for (const link of linkElements) {
@@ -168,7 +167,7 @@ class LinkElements extends FRGatherer {
168
167
  * @return {Promise<LH.Artifacts['LinkElements']>}
169
168
  */
170
169
  async afterPass(context, loadData) {
171
- return this._getArtifact({...context, dependencies: {}}, loadData.networkRecords);
170
+ return this._getArtifact({...context, dependencies: {}}, loadData.devtoolsLog);
172
171
  }
173
172
 
174
173
  /**
@@ -176,8 +175,7 @@ class LinkElements extends FRGatherer {
176
175
  * @return {Promise<LH.Artifacts['LinkElements']>}
177
176
  */
178
177
  async getArtifact(context) {
179
- const records = await NetworkRecords.request(context.dependencies.DevtoolsLog, context);
180
- return this._getArtifact(context, records);
178
+ return this._getArtifact(context, context.dependencies.DevtoolsLog);
181
179
  }
182
180
  }
183
181
 
@@ -6,10 +6,9 @@
6
6
  'use strict';
7
7
 
8
8
  const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
9
- const NetworkAnalyzer = require('../../lib/dependency-graph/simulator/network-analyzer.js');
10
- const NetworkRecords = require('../../computed/network-records.js');
11
9
  const DevtoolsLog = require('./devtools-log.js');
12
10
  const {fetchResponseBodyFromCache} = require('../driver/network.js');
11
+ const MainResource = require('../../computed/main-resource.js');
13
12
 
14
13
  /**
15
14
  * Collects the content of the main html document.
@@ -24,14 +23,16 @@ class MainDocumentContent extends FRGatherer {
24
23
  /**
25
24
  *
26
25
  * @param {LH.Gatherer.FRTransitionalContext} context
27
- * @param {LH.Artifacts.NetworkRequest[]} networkRecords
26
+ * @param {LH.Artifacts['DevtoolsLog']} devtoolsLog
28
27
  * @return {Promise<LH.Artifacts['MainDocumentContent']>}
29
28
  */
30
- async _getArtifact(context, networkRecords) {
31
- const mainResource = NetworkAnalyzer.findMainDocument(networkRecords, context.url);
29
+ async _getArtifact(context, devtoolsLog) {
30
+ const mainResource =
31
+ await MainResource.request({devtoolsLog, URL: context.baseArtifacts.URL}, context);
32
32
  const session = context.driver.defaultSession;
33
33
  return fetchResponseBodyFromCache(session, mainResource.requestId);
34
34
  }
35
+
35
36
  /**
36
37
  *
37
38
  * @param {LH.Gatherer.FRTransitionalContext<'DevtoolsLog'>} context
@@ -39,8 +40,7 @@ class MainDocumentContent extends FRGatherer {
39
40
  */
40
41
  async getArtifact(context) {
41
42
  const devtoolsLog = context.dependencies.DevtoolsLog;
42
- const networkRecords = await NetworkRecords.request(devtoolsLog, context);
43
- return this._getArtifact(context, networkRecords);
43
+ return this._getArtifact(context, devtoolsLog);
44
44
  }
45
45
 
46
46
  /**
@@ -49,7 +49,7 @@ class MainDocumentContent extends FRGatherer {
49
49
  * @return {Promise<LH.Artifacts['MainDocumentContent']>}
50
50
  */
51
51
  async afterPass(passContext, loadData) {
52
- return this._getArtifact({...passContext, dependencies: {}}, loadData.networkRecords);
52
+ return this._getArtifact({...passContext, dependencies: {}}, loadData.devtoolsLog);
53
53
  }
54
54
  }
55
55
 
@@ -7,11 +7,10 @@
7
7
 
8
8
  /**
9
9
  * @param {LH.Artifacts.NetworkRequest[]} networkRecords
10
- * @param {LH.Artifacts.Script|undefined} script
10
+ * @param {LH.Artifacts.Script} script
11
11
  * @return {LH.Artifacts.NetworkRequest|undefined}
12
12
  */
13
13
  function getRequestForScript(networkRecords, script) {
14
- if (!script) return;
15
14
  let networkRequest = networkRecords.find(request => request.url === script.url);
16
15
  while (networkRequest?.redirectDestination) {
17
16
  networkRequest = networkRequest.redirectDestination;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.5.0-dev.20220321",
3
+ "version": "9.5.0-dev.20220322",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -93,29 +93,54 @@ class ReportGenerator {
93
93
  const separator = ',';
94
94
  /** @param {string} value @return {string} */
95
95
  const escape = value => `"${value.replace(/"/g, '""')}"`;
96
- /** @param {Array<string | number>} row @return {string[]} */
97
- const rowFormatter = row => row.map(value => value.toString()).map(escape);
98
-
99
- // Possible TODO: tightly couple headers and row values
100
- const header = ['requestedUrl', 'finalUrl', 'category', 'name', 'title', 'type', 'score'];
101
- const table = Object.keys(lhr.categories).map(categoryId => {
102
- const rows = [];
103
- const category = lhr.categories[categoryId];
104
- const overallCategoryScore = category.score === null ? -1 : category.score;
105
- rows.push(rowFormatter([lhr.requestedUrl, lhr.finalUrl, category.title,
106
- `${categoryId}-score`, `Overall ${category.title} Category Score`, 'numeric',
107
- overallCategoryScore]));
108
- return rows.concat(category.auditRefs.map(auditRef => {
96
+ /** @param {ReadonlyArray<string | number | null>} row @return {string[]} */
97
+ const rowFormatter = row => row.map(value => {
98
+ if (value === null) return 'null';
99
+ return value.toString();
100
+ }).map(escape);
101
+
102
+ const rows = [];
103
+ const topLevelKeys = /** @type {const} */(
104
+ ['requestedUrl', 'finalUrl', 'fetchTime', 'gatherMode']);
105
+
106
+ // First we have metadata about the LHR.
107
+ rows.push(rowFormatter(topLevelKeys));
108
+ rows.push(rowFormatter(topLevelKeys.map(key => lhr[key])));
109
+
110
+ // Some spacing.
111
+ rows.push([]);
112
+
113
+ // Categories.
114
+ rows.push(['category', 'score']);
115
+ for (const category of Object.values(lhr.categories)) {
116
+ rows.push(rowFormatter([
117
+ category.id,
118
+ category.score,
119
+ ]));
120
+ }
121
+
122
+ rows.push([]);
123
+
124
+ // Audits.
125
+ rows.push(['category', 'audit', 'score', 'displayValue', 'description']);
126
+ for (const category of Object.values(lhr.categories)) {
127
+ for (const auditRef of category.auditRefs) {
109
128
  const audit = lhr.audits[auditRef.id];
110
- // CSV validator wants all scores to be numeric, use -1 for now
111
- const numericScore = audit.score === null ? -1 : audit.score;
112
- return rowFormatter([lhr.requestedUrl, lhr.finalUrl, category.title, audit.id, audit.title,
113
- audit.scoreDisplayMode, numericScore]);
114
- }));
115
- });
129
+ if (!audit) continue;
130
+
131
+ rows.push(rowFormatter([
132
+ category.id,
133
+ auditRef.id,
134
+ audit.score,
135
+ audit.displayValue || '',
136
+ audit.description,
137
+ ]));
138
+ }
139
+ }
116
140
 
117
- return [header].concat(...table)
118
- .map(row => row.join(separator)).join(CRLF);
141
+ return rows
142
+ .map(row => row.join(separator))
143
+ .join(CRLF);
119
144
  }
120
145
 
121
146
  /**
@@ -5,6 +5,8 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
+ /* eslint-disable no-irregular-whitespace */
9
+
8
10
  const assert = require('assert').strict;
9
11
  const fs = require('fs');
10
12
 
@@ -81,28 +83,33 @@ describe('ReportGenerator', () => {
81
83
 
82
84
  it('creates CSV for results', async () => {
83
85
  const path = './.results-as-csv.csv';
84
- const headers = {
85
- category: '',
86
- name: '',
87
- title: '',
88
- type: '',
89
- score: 42,
90
- };
91
86
 
92
87
  const csvOutput = ReportGenerator.generateReport(sampleResults, 'csv');
93
88
  fs.writeFileSync(path, csvOutput);
94
89
 
95
90
  const lines = csvOutput.split('\n');
96
91
  expect(lines.length).toBeGreaterThan(100);
97
- expect(lines.slice(0, 3).join('\n')).toMatchInlineSnapshot(`
98
- "requestedUrl,finalUrl,category,name,title,type,score
99
- \\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"Performance\\",\\"performance-score\\",\\"Overall Performance Category Score\\",\\"numeric\\",\\"0.26\\"
100
- \\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"Performance\\",\\"first-contentful-paint\\",\\"First Contentful Paint\\",\\"numeric\\",\\"0.01\\"
92
+ expect(lines.slice(0, 15).join('\n')).toMatchInlineSnapshot(`
93
+ "\\"requestedUrl\\",\\"finalUrl\\",\\"fetchTime\\",\\"gatherMode\\"
94
+ \\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"2021-09-07T20:11:11.853Z\\",\\"navigation\\"
95
+
96
+ category,score
97
+ \\"performance\\",\\"0.26\\"
98
+ \\"accessibility\\",\\"0.78\\"
99
+ \\"best-practices\\",\\"0.25\\"
100
+ \\"seo\\",\\"0.67\\"
101
+ \\"pwa\\",\\"0.3\\"
102
+
103
+ category,audit,score,displayValue,description
104
+ \\"performance\\",\\"first-contentful-paint\\",\\"0.01\\",\\"6.8 s\\",\\"First Contentful Paint marks the time at which the first text or image is painted. [Learn more](https://web.dev/first-contentful-paint/).\\"
105
+ \\"performance\\",\\"interactive\\",\\"0.41\\",\\"8.2 s\\",\\"Time to interactive is the amount of time it takes for the page to become fully interactive. [Learn more](https://web.dev/interactive/).\\"
106
+ \\"performance\\",\\"speed-index\\",\\"0.21\\",\\"8.1 s\\",\\"Speed Index shows how quickly the contents of a page are visibly populated. [Learn more](https://web.dev/speed-index/).\\"
107
+ \\"performance\\",\\"total-blocking-time\\",\\"0.2\\",\\"1,220 ms\\",\\"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/).\\"
101
108
  "
102
109
  `);
103
110
 
104
111
  try {
105
- await csvValidator(path, headers);
112
+ await csvValidator(path);
106
113
  } catch (err) {
107
114
  assert.fail('CSV parser error:\n' + err.join('\n'));
108
115
  } finally {
@@ -110,13 +117,13 @@ describe('ReportGenerator', () => {
110
117
  }
111
118
  });
112
119
 
113
- it('creates CSV for results including overall category scores', () => {
120
+ it('creates CSV for results including categories', () => {
114
121
  const csvOutput = ReportGenerator.generateReport(sampleResults, 'csv');
115
- expect(csvOutput).toContain('performance-score');
116
- expect(csvOutput).toContain('accessibility-score');
117
- expect(csvOutput).toContain('best-practices-score');
118
- expect(csvOutput).toContain('seo-score');
119
- expect(csvOutput).toContain('pwa-score');
122
+ expect(csvOutput).toContain('performance');
123
+ expect(csvOutput).toContain('accessibility');
124
+ expect(csvOutput).toContain('best-practices');
125
+ expect(csvOutput).toContain('seo');
126
+ expect(csvOutput).toContain('pwa');
120
127
  });
121
128
 
122
129
  it('writes extended info', () => {