lighthouse 9.6.2 → 9.6.5

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.
@@ -8,6 +8,7 @@
8
8
  const Audit = require('./audit.js');
9
9
  const URL = require('../lib/url-shim.js');
10
10
  const NetworkRecords = require('../computed/network-records.js');
11
+ const MainResource = require('../computed/main-resource.js');
11
12
 
12
13
  class NetworkRequests extends Audit {
13
14
  /**
@@ -19,7 +20,7 @@ class NetworkRequests extends Audit {
19
20
  scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
20
21
  title: 'Network Requests',
21
22
  description: 'Lists the network requests that were made during page load.',
22
- requiredArtifacts: ['devtoolsLogs'],
23
+ requiredArtifacts: ['devtoolsLogs', 'URL', 'GatherContext'],
23
24
  };
24
25
  }
25
26
 
@@ -28,75 +29,99 @@ class NetworkRequests extends Audit {
28
29
  * @param {LH.Audit.Context} context
29
30
  * @return {Promise<LH.Audit.Product>}
30
31
  */
31
- static audit(artifacts, context) {
32
+ static async audit(artifacts, context) {
32
33
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
33
- return NetworkRecords.request(devtoolsLog, context).then(records => {
34
- const earliestStartTime = records.reduce(
35
- (min, record) => Math.min(min, record.startTime),
36
- Infinity
37
- );
34
+ const records = await NetworkRecords.request(devtoolsLog, context);
35
+ const earliestStartTime = records.reduce(
36
+ (min, record) => Math.min(min, record.startTime),
37
+ Infinity
38
+ );
38
39
 
39
- /** @param {number} time */
40
- const timeToMs = time => time < earliestStartTime || !Number.isFinite(time) ?
41
- undefined : (time - earliestStartTime) * 1000;
40
+ // Optional mainFrameId check because the main resource is only available for
41
+ // navigations. TODO: https://github.com/GoogleChrome/lighthouse/issues/14157
42
+ // for the general solution to this.
43
+ /** @type {string|undefined} */
44
+ let mainFrameId;
45
+ if (artifacts.GatherContext.gatherMode === 'navigation') {
46
+ const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
47
+ mainFrameId = mainResource.frameId;
48
+ }
42
49
 
43
- const results = records.map(record => {
44
- const endTimeDeltaMs = record.lrStatistics?.endTimeDeltaMs;
45
- const TCPMs = record.lrStatistics?.TCPMs;
46
- const requestMs = record.lrStatistics?.requestMs;
47
- const responseMs = record.lrStatistics?.responseMs;
50
+ /** @param {number} time */
51
+ const timeToMs = time => time < earliestStartTime || !Number.isFinite(time) ?
52
+ undefined : (time - earliestStartTime) * 1000;
48
53
 
49
- return {
50
- url: URL.elideDataURI(record.url),
51
- protocol: record.protocol,
52
- startTime: timeToMs(record.startTime),
53
- endTime: timeToMs(record.endTime),
54
- finished: record.finished,
55
- transferSize: record.transferSize,
56
- resourceSize: record.resourceSize,
57
- statusCode: record.statusCode,
58
- mimeType: record.mimeType,
59
- resourceType: record.resourceType,
60
- lrEndTimeDeltaMs: endTimeDeltaMs, // Only exists on Lightrider runs
61
- lrTCPMs: TCPMs, // Only exists on Lightrider runs
62
- lrRequestMs: requestMs, // Only exists on Lightrider runs
63
- lrResponseMs: responseMs, // Only exists on Lightrider runs
64
- };
65
- });
66
-
67
- // NOTE(i18n): this audit is only for debug info in the LHR and does not appear in the report.
68
- /** @type {LH.Audit.Details.Table['headings']} */
69
- const headings = [
70
- {key: 'url', itemType: 'url', text: 'URL'},
71
- {key: 'protocol', itemType: 'text', text: 'Protocol'},
72
- {key: 'startTime', itemType: 'ms', granularity: 1, text: 'Start Time'},
73
- {key: 'endTime', itemType: 'ms', granularity: 1, text: 'End Time'},
74
- {
75
- key: 'transferSize',
76
- itemType: 'bytes',
77
- displayUnit: 'kb',
78
- granularity: 1,
79
- text: 'Transfer Size',
80
- },
81
- {
82
- key: 'resourceSize',
83
- itemType: 'bytes',
84
- displayUnit: 'kb',
85
- granularity: 1,
86
- text: 'Resource Size',
87
- },
88
- {key: 'statusCode', itemType: 'text', text: 'Status Code'},
89
- {key: 'mimeType', itemType: 'text', text: 'MIME Type'},
90
- {key: 'resourceType', itemType: 'text', text: 'Resource Type'},
91
- ];
92
-
93
- const tableDetails = Audit.makeTableDetails(headings, results);
54
+ const results = records.map(record => {
55
+ const endTimeDeltaMs = record.lrStatistics?.endTimeDeltaMs;
56
+ const TCPMs = record.lrStatistics?.TCPMs;
57
+ const requestMs = record.lrStatistics?.requestMs;
58
+ const responseMs = record.lrStatistics?.responseMs;
59
+ // Default these to undefined so omitted from JSON in the negative case.
60
+ const isLinkPreload = record.isLinkPreload || undefined;
61
+ const experimentalFromMainFrame = mainFrameId ?
62
+ ((record.frameId === mainFrameId) || undefined) :
63
+ undefined;
94
64
 
95
65
  return {
96
- score: 1,
97
- details: tableDetails,
66
+ url: URL.elideDataURI(record.url),
67
+ protocol: record.protocol,
68
+ startTime: timeToMs(record.startTime),
69
+ endTime: timeToMs(record.endTime),
70
+ finished: record.finished,
71
+ transferSize: record.transferSize,
72
+ resourceSize: record.resourceSize,
73
+ statusCode: record.statusCode,
74
+ mimeType: record.mimeType,
75
+ resourceType: record.resourceType,
76
+ isLinkPreload,
77
+ experimentalFromMainFrame,
78
+ lrEndTimeDeltaMs: endTimeDeltaMs, // Only exists on Lightrider runs
79
+ lrTCPMs: TCPMs, // Only exists on Lightrider runs
80
+ lrRequestMs: requestMs, // Only exists on Lightrider runs
81
+ lrResponseMs: responseMs, // Only exists on Lightrider runs
98
82
  };
99
83
  });
84
+
85
+ // NOTE(i18n): this audit is only for debug info in the LHR and does not appear in the report.
86
+ /** @type {LH.Audit.Details.Table['headings']} */
87
+ const headings = [
88
+ {key: 'url', itemType: 'url', text: 'URL'},
89
+ {key: 'protocol', itemType: 'text', text: 'Protocol'},
90
+ {key: 'startTime', itemType: 'ms', granularity: 1, text: 'Start Time'},
91
+ {key: 'endTime', itemType: 'ms', granularity: 1, text: 'End Time'},
92
+ {
93
+ key: 'transferSize',
94
+ itemType: 'bytes',
95
+ displayUnit: 'kb',
96
+ granularity: 1,
97
+ text: 'Transfer Size',
98
+ },
99
+ {
100
+ key: 'resourceSize',
101
+ itemType: 'bytes',
102
+ displayUnit: 'kb',
103
+ granularity: 1,
104
+ text: 'Resource Size',
105
+ },
106
+ {key: 'statusCode', itemType: 'text', text: 'Status Code'},
107
+ {key: 'mimeType', itemType: 'text', text: 'MIME Type'},
108
+ {key: 'resourceType', itemType: 'text', text: 'Resource Type'},
109
+ ];
110
+
111
+ const tableDetails = Audit.makeTableDetails(headings, results);
112
+
113
+ // Include starting timestamp to allow syncing requests with navStart/metric timestamps.
114
+ const networkStartTimeTs = Number.isFinite(earliestStartTime) ?
115
+ earliestStartTime * 1_000_000 : undefined;
116
+ tableDetails.debugData = {
117
+ type: 'debugdata',
118
+ networkStartTimeTs,
119
+ };
120
+
121
+ return {
122
+ score: 1,
123
+ details: tableDetails,
124
+ };
100
125
  }
101
126
  }
102
127
 
@@ -23,6 +23,10 @@ const UIStrings = {
23
23
 
24
24
  const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
25
25
 
26
+ /**
27
+ * @typedef {Array<{url: string, initiatorType: string}>} InitiatorPath
28
+ */
29
+
26
30
  class PreloadLCPImageAudit extends Audit {
27
31
  /**
28
32
  * @return {LH.Audit.Meta}
@@ -86,22 +90,33 @@ class PreloadLCPImageAudit extends Audit {
86
90
  * @param {LH.Gatherer.Simulation.GraphNode} graph
87
91
  * @param {LH.Artifacts.TraceElement|undefined} lcpElement
88
92
  * @param {Array<LH.Artifacts.ImageElement>} imageElements
89
- * @return {LH.Gatherer.Simulation.GraphNetworkNode|undefined}
93
+ * @return {{lcpNodeToPreload?: LH.Gatherer.Simulation.GraphNetworkNode, initiatorPath?: InitiatorPath}}
90
94
  */
91
95
  static getLCPNodeToPreload(mainResource, graph, lcpElement, imageElements) {
92
- if (!lcpElement) return undefined;
96
+ if (!lcpElement) return {};
93
97
 
94
98
  const lcpImageElement = imageElements.find(elem => {
95
99
  return elem.node.devtoolsNodePath === lcpElement.node.devtoolsNodePath;
96
100
  });
97
101
 
98
- if (!lcpImageElement) return undefined;
102
+ if (!lcpImageElement) return {};
99
103
  const lcpUrl = lcpImageElement.src;
100
104
  const {lcpNode, path} = PreloadLCPImageAudit.findLCPNode(graph, lcpUrl);
101
- if (!lcpNode || !path) return undefined;
105
+ if (!lcpNode || !path) return {};
106
+
102
107
  // eslint-disable-next-line max-len
103
108
  const shouldPreload = PreloadLCPImageAudit.shouldPreloadRequest(lcpNode.record, mainResource, path);
104
- return shouldPreload ? lcpNode : undefined;
109
+ const lcpNodeToPreload = shouldPreload ? lcpNode : undefined;
110
+
111
+ const initiatorPath = [
112
+ {url: lcpNode.record.url, initiatorType: lcpNode.initiatorType},
113
+ ...path.map(n => ({url: n.record.url, initiatorType: n.initiatorType})),
114
+ ];
115
+
116
+ return {
117
+ lcpNodeToPreload,
118
+ initiatorPath,
119
+ };
105
120
  }
106
121
 
107
122
  /**
@@ -215,10 +230,10 @@ class PreloadLCPImageAudit extends Audit {
215
230
 
216
231
  const graph = lanternLCP.pessimisticGraph;
217
232
  // eslint-disable-next-line max-len
218
- const lcpNode = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
233
+ const {lcpNodeToPreload, initiatorPath} = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
219
234
 
220
235
  const {results, wastedMs} =
221
- PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNode, graph, simulator);
236
+ PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNodeToPreload, graph, simulator);
222
237
 
223
238
  /** @type {LH.Audit.Details.Opportunity['headings']} */
224
239
  const headings = [
@@ -228,6 +243,17 @@ class PreloadLCPImageAudit extends Audit {
228
243
  ];
229
244
  const details = Audit.makeOpportunityDetails(headings, results, wastedMs);
230
245
 
246
+ // If LCP element was an image and had valid network records (regardless of
247
+ // if it should be preloaded), it will be found first in the `initiatorPath`.
248
+ // Otherwise path and length will be undefined.
249
+ if (initiatorPath) {
250
+ details.debugData = {
251
+ type: 'debugdata',
252
+ initiatorPath,
253
+ pathLength: initiatorPath.length,
254
+ };
255
+ }
256
+
231
257
  return {
232
258
  score: UnusedBytes.scoreForWastedMs(wastedMs),
233
259
  numericValue: wastedMs,
@@ -17,7 +17,8 @@ class MainThreadTasks {
17
17
  */
18
18
  static async compute_(trace, context) {
19
19
  const {mainThreadEvents, frames, timestamps} = await ProcessedTrace.request(trace, context);
20
- return MainThreadTasks_.getMainThreadTasks(mainThreadEvents, frames, timestamps.traceEnd);
20
+ return MainThreadTasks_.getMainThreadTasks(mainThreadEvents, frames, timestamps.traceEnd,
21
+ timestamps.timeOrigin);
21
22
  }
22
23
  }
23
24
 
@@ -44,7 +44,7 @@ class NetworkNode extends BaseNode {
44
44
  }
45
45
 
46
46
  /**
47
- * @return {?string}
47
+ * @return {string}
48
48
  */
49
49
  get initiatorType() {
50
50
  return this._record.initiator && this._record.initiator.type;
@@ -561,9 +561,10 @@ class MainThreadTasks {
561
561
  * @param {LH.TraceEvent[]} mainThreadEvents
562
562
  * @param {Array<{id: string, url: string}>} frames
563
563
  * @param {number} traceEndTs
564
+ * @param {number} [traceStartTs] Optional time-0 ts for tasks. Tasks before this point will have negative start/end times. Defaults to the first task found.
564
565
  * @return {TaskNode[]}
565
566
  */
566
- static getMainThreadTasks(mainThreadEvents, frames, traceEndTs) {
567
+ static getMainThreadTasks(mainThreadEvents, frames, traceEndTs, traceStartTs) {
567
568
  const timers = new Map();
568
569
  const xhrs = new Map();
569
570
  const frameURLsById = new Map();
@@ -587,8 +588,8 @@ class MainThreadTasks {
587
588
  priorTaskData.lastTaskURLs = task.attributableURLs;
588
589
  }
589
590
 
590
- // Rebase all the times to be relative to start of trace in ms
591
- const firstTs = (tasks[0] || {startTime: 0}).startTime;
591
+ // Rebase all the times to be relative to start of trace and covert to ms.
592
+ const firstTs = traceStartTs ?? tasks[0].startTime;
592
593
  for (const task of tasks) {
593
594
  task.startTime = (task.startTime - firstTs) / 1000;
594
595
  task.endTime = (task.endTime - firstTs) / 1000;
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "module",
3
+ "//": "Any directory that uses `import ... from` or `export ...` must be type module. Temporary file until root package.json is type: module"
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.6.2",
3
+ "version": "9.6.5",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -192,7 +192,7 @@
192
192
  "jpeg-js": "^0.4.3",
193
193
  "js-library-detector": "^6.5.0",
194
194
  "lighthouse-logger": "^1.3.0",
195
- "lighthouse-stack-packs": "^1.8.1",
195
+ "lighthouse-stack-packs": "1.8.2",
196
196
  "lodash": "^4.17.21",
197
197
  "lookup-closest-locale": "6.2.0",
198
198
  "metaviewport-parser": "0.2.0",
@@ -2586,7 +2586,7 @@
2586
2586
  "message": "If you are using React Router, minimize usage of the `<Redirect>` component for [route navigations](https://reacttraining.com/react-router/web/api/Redirect)."
2587
2587
  },
2588
2588
  "node_modules/lighthouse-stack-packs/packs/react.js | server-response-time": {
2589
- "message": "If you are server-side rendering any React components, consider using `renderToNodeStream()` or `renderToStaticNodeStream()` to allow the client to receive and hydrate different parts of the markup instead of all at once. [Learn more](https://reactjs.org/docs/react-dom-server.html#rendertonodestream)."
2589
+ "message": "If you are server-side rendering any React components, consider using `renderToPipeableStream()` or `renderToStaticNodeStream()` to allow the client to receive and hydrate different parts of the markup instead of all at once. [Learn more](https://reactjs.org/docs/react-dom-server.html#renderToPipeableStream)."
2590
2590
  },
2591
2591
  "node_modules/lighthouse-stack-packs/packs/react.js | unminified-css": {
2592
2592
  "message": "If your build system minifies CSS files automatically, ensure that you are deploying the production build of your application. You can check this with the React Developer Tools extension. [Learn more](https://reactjs.org/docs/optimizing-performance.html#use-the-production-build)."
@@ -2604,7 +2604,7 @@
2604
2604
  "message": "Consider uploading your GIF to a service which will make it available to embed as an HTML5 video."
2605
2605
  },
2606
2606
  "node_modules/lighthouse-stack-packs/packs/wordpress.js | modern-image-formats": {
2607
- "message": "Consider using a [plugin](https://wordpress.org/plugins/search/convert+webp/) or service that will automatically convert your uploaded images to the optimal formats."
2607
+ "message": "Consider using the [Performance Lab](https://wordpress.org/plugins/performance-lab/) plugin to automatically convert your uploaded JPEG images into WebP, wherever supported."
2608
2608
  },
2609
2609
  "node_modules/lighthouse-stack-packs/packs/wordpress.js | offscreen-images": {
2610
2610
  "message": "Install a [lazy-load WordPress plugin](https://wordpress.org/plugins/search/lazy+load/) that provides the ability to defer any offscreen images, or switch to a theme that provides that functionality. Also consider using [the AMP plugin](https://wordpress.org/plugins/amp/)."
@@ -2586,7 +2586,7 @@
2586
2586
  "message": "Îf́ ŷóû ár̂é ûśîńĝ Ŕêáĉt́ R̂óût́êŕ, m̂ín̂ím̂íẑé ûśâǵê óf̂ t́ĥé `<Redirect>` ĉóm̂ṕôńêńt̂ f́ôŕ [r̂óût́ê ńâv́îǵât́îón̂ś](https://reacttraining.com/react-router/web/api/Redirect)."
2587
2587
  },
2588
2588
  "node_modules/lighthouse-stack-packs/packs/react.js | server-response-time": {
2589
- "message": "Îf́ ŷóû ár̂é ŝér̂v́êŕ-ŝíd̂é r̂én̂d́êŕîńĝ án̂ý R̂éâćt̂ ćôḿp̂ón̂én̂t́ŝ, ćôńŝíd̂ér̂ úŝín̂ǵ `renderToNodeStream()` ôŕ `renderToStaticNodeStream()` t̂ó âĺl̂óŵ t́ĥé ĉĺîén̂t́ t̂ó r̂éĉéîv́ê án̂d́ ĥýd̂ŕât́ê d́îf́f̂ér̂én̂t́ p̂ár̂t́ŝ óf̂ t́ĥé m̂ár̂ḱûṕ îńŝt́êád̂ óf̂ ál̂ĺ ât́ ôńĉé. [L̂éâŕn̂ ḿôŕê](https://reactjs.org/docs/react-dom-server.html#rendertonodestream)."
2589
+ "message": "Îf́ ŷóû ár̂é ŝér̂v́êŕ-ŝíd̂é r̂én̂d́êŕîńĝ án̂ý R̂éâćt̂ ćôḿp̂ón̂én̂t́ŝ, ćôńŝíd̂ér̂ úŝín̂ǵ `renderToPipeableStream()` ôŕ `renderToStaticNodeStream()` t̂ó âĺl̂óŵ t́ĥé ĉĺîén̂t́ t̂ó r̂éĉéîv́ê án̂d́ ĥýd̂ŕât́ê d́îf́f̂ér̂én̂t́ p̂ár̂t́ŝ óf̂ t́ĥé m̂ár̂ḱûṕ îńŝt́êád̂ óf̂ ál̂ĺ ât́ ôńĉé. [L̂éâŕn̂ ḿôŕê](https://reactjs.org/docs/react-dom-server.html#renderToPipeableStream)."
2590
2590
  },
2591
2591
  "node_modules/lighthouse-stack-packs/packs/react.js | unminified-css": {
2592
2592
  "message": "Îf́ ŷóûŕ b̂úîĺd̂ śŷśt̂ém̂ ḿîńîf́îéŝ ĆŜŚ f̂íl̂éŝ áût́ôḿât́îćâĺl̂ý, êńŝúr̂é t̂h́ât́ ŷóû ár̂é d̂ép̂ĺôýîńĝ t́ĥé p̂ŕôd́ûćt̂íôń b̂úîĺd̂ óf̂ ýôúr̂ áp̂ṕl̂íĉát̂íôń. Ŷóû ćâń ĉh́êćk̂ t́ĥíŝ ẃît́ĥ t́ĥé R̂éâćt̂ D́êv́êĺôṕêŕ T̂óôĺŝ éx̂t́êńŝíôń. [L̂éâŕn̂ ḿôŕê](https://reactjs.org/docs/optimizing-performance.html#use-the-production-build)."
@@ -2604,7 +2604,7 @@
2604
2604
  "message": "Ĉón̂śîd́êŕ ûṕl̂óâd́îńĝ ýôúr̂ ǴÎF́ t̂ó â śêŕv̂íĉé ŵh́îćĥ ẃîĺl̂ ḿâḱê ít̂ áv̂áîĺâb́l̂é t̂ó êḿb̂éd̂ áŝ án̂ H́T̂ḾL̂5 v́îd́êó."
2605
2605
  },
2606
2606
  "node_modules/lighthouse-stack-packs/packs/wordpress.js | modern-image-formats": {
2607
- "message": "Ĉón̂śîd́êŕ ûśîńĝ á [p̂ĺûǵîń](https://wordpress.org/plugins/search/convert+webp/) ôŕ ŝér̂v́îćê ĥát̂ ẃîĺl̂ áût́ôḿât́îćâĺl̂ý ĉón̂v́êŕt̂ ýôú úp̂ĺôád̂ém̂áĝéŝ t́ô t́é ôṕt̂ím̂ál̂ f́ôŕm̂át̂ś."
2607
+ "message": "Ĉón̂śîd́êŕ ûśîńĝ t́ĥé [P̂ér̂f́ôŕm̂án̂ćê Ĺâb́](https://wordpress.org/plugins/performance-lab/) p̂ĺûǵîń t̂ó ât̂óm̂át̂íĉál̂ĺŷ ćôńv̂ér̂t́ ŷóûŕ ûṕl̂âd́êd́ ĴṔÊǴ îḿâǵêś îńt̂ó Ŵéb̂Ṕ, ŵh́êŕêv́êŕ ŝúp̂ṕôŕt̂éd̂."
2608
2608
  },
2609
2609
  "node_modules/lighthouse-stack-packs/packs/wordpress.js | offscreen-images": {
2610
2610
  "message": "Îńŝt́âĺl̂ á [l̂áẑý-l̂óâd́ Ŵór̂d́P̂ŕêśŝ ṕl̂úĝín̂](https://wordpress.org/plugins/search/lazy+load/) t́ĥát̂ ṕr̂óv̂íd̂éŝ t́ĥé âb́îĺît́ŷ t́ô d́êf́êŕ âńŷ óf̂f́ŝćr̂éêń îḿâǵêś, ôŕ ŝẃît́ĉh́ t̂ó â t́ĥém̂é t̂h́ât́ p̂ŕôv́îd́êś t̂h́ât́ f̂ún̂ćt̂íôńâĺît́ŷ. Ál̂śô ćôńŝíd̂ér̂ úŝín̂ǵ [t̂h́ê ÁM̂Ṕ p̂ĺûǵîń](https://wordpress.org/plugins/amp/)."