lighthouse 11.7.0-dev.20240410 → 11.7.0-dev.20240411

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.
Files changed (37) hide show
  1. package/core/audits/byte-efficiency/render-blocking-resources.js +25 -26
  2. package/core/audits/layout-shifts.js +1 -1
  3. package/core/computed/metrics/lantern-first-contentful-paint.js +3 -2
  4. package/core/computed/metrics/lantern-first-meaningful-paint.js +3 -2
  5. package/core/computed/metrics/lantern-interactive.js +3 -2
  6. package/core/computed/metrics/lantern-largest-contentful-paint.js +3 -2
  7. package/core/computed/metrics/lantern-max-potential-fid.js +3 -2
  8. package/core/computed/metrics/lantern-metric.d.ts +5 -0
  9. package/core/computed/metrics/lantern-metric.js +23 -1
  10. package/core/computed/metrics/lantern-speed-index.js +3 -2
  11. package/core/computed/navigation-insights.d.ts +36 -0
  12. package/core/computed/navigation-insights.js +35 -0
  13. package/core/computed/trace-engine-result.d.ts +16 -3
  14. package/core/computed/trace-engine-result.js +19 -14
  15. package/core/config/default-config.js +0 -1
  16. package/core/config/filters.js +0 -1
  17. package/core/gather/gatherers/root-causes.d.ts +2 -2
  18. package/core/gather/gatherers/root-causes.js +6 -5
  19. package/core/gather/gatherers/trace-elements.d.ts +2 -2
  20. package/core/gather/gatherers/trace-elements.js +2 -2
  21. package/core/lib/lantern/cpu-node.d.ts +7 -1
  22. package/core/lib/lantern/cpu-node.js +12 -2
  23. package/core/lib/lantern/lantern-error.d.ts +8 -0
  24. package/core/lib/lantern/lantern-error.js +9 -0
  25. package/core/lib/lantern/metrics/first-meaningful-paint.js +2 -4
  26. package/core/lib/lantern/metrics/interactive.js +1 -1
  27. package/core/lib/lantern/metrics/largest-contentful-paint.js +3 -3
  28. package/core/lib/lantern/page-dependency-graph.js +14 -2
  29. package/core/lib/lantern/simulator/simulator.js +1 -1
  30. package/core/lib/lh-error.js +3 -3
  31. package/core/runner.js +0 -1
  32. package/package.json +1 -1
  33. package/tsconfig.json +0 -1
  34. package/types/artifacts.d.ts +15 -63
  35. package/types/lhr/lhr.d.ts +0 -2
  36. package/core/gather/gatherers/dobetterweb/tags-blocking-first-paint.d.ts +0 -47
  37. package/core/gather/gatherers/dobetterweb/tags-blocking-first-paint.js +0 -233
@@ -6,8 +6,6 @@
6
6
 
7
7
  import * as Lantern from '../types/lantern.js';
8
8
  import {Metric} from '../metric.js';
9
- // TODO(15841): don't use LighthouseError
10
- import {LighthouseError} from '../../lh-error.js';
11
9
  import {FirstContentfulPaint} from './first-contentful-paint.js';
12
10
 
13
11
  /** @typedef {import('../base-node.js').Node} Node */
@@ -32,7 +30,7 @@ class FirstMeaningfulPaint extends Metric {
32
30
  static getOptimisticGraph(dependencyGraph, processedNavigation) {
33
31
  const fmp = processedNavigation.timestamps.firstMeaningfulPaint;
34
32
  if (!fmp) {
35
- throw new LighthouseError(LighthouseError.errors.NO_FMP);
33
+ throw new Error('NO_FMP');
36
34
  }
37
35
  return FirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, {
38
36
  cutoffTimestamp: fmp,
@@ -51,7 +49,7 @@ class FirstMeaningfulPaint extends Metric {
51
49
  static getPessimisticGraph(dependencyGraph, processedNavigation) {
52
50
  const fmp = processedNavigation.timestamps.firstMeaningfulPaint;
53
51
  if (!fmp) {
54
- throw new LighthouseError(LighthouseError.errors.NO_FMP);
52
+ throw new Error('NO_FMP');
55
53
  }
56
54
 
57
55
  return FirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, {
@@ -37,7 +37,7 @@ class Interactive extends Metric {
37
37
  return dependencyGraph.cloneWithRelationships(node => {
38
38
  // Include everything that might be a long task
39
39
  if (node.type === BaseNode.TYPES.CPU) {
40
- return node.event.dur > minimumCpuTaskDuration;
40
+ return node.duration > minimumCpuTaskDuration;
41
41
  }
42
42
 
43
43
  // Include all scripts and high priority requests, exclude all images
@@ -6,8 +6,8 @@
6
6
 
7
7
  import * as Lantern from '../types/lantern.js';
8
8
  import {Metric} from '../metric.js';
9
- import {LighthouseError} from '../../../lib/lh-error.js';
10
9
  import {FirstContentfulPaint} from './first-contentful-paint.js';
10
+ import {LanternError} from '../lantern-error.js';
11
11
 
12
12
  /** @typedef {import('../base-node.js').Node} Node */
13
13
 
@@ -45,7 +45,7 @@ class LargestContentfulPaint extends Metric {
45
45
  static getOptimisticGraph(dependencyGraph, processedNavigation) {
46
46
  const lcp = processedNavigation.timestamps.largestContentfulPaint;
47
47
  if (!lcp) {
48
- throw new LighthouseError(LighthouseError.errors.NO_LCP);
48
+ throw new LanternError('NO_LCP');
49
49
  }
50
50
 
51
51
  return FirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, {
@@ -62,7 +62,7 @@ class LargestContentfulPaint extends Metric {
62
62
  static getPessimisticGraph(dependencyGraph, processedNavigation) {
63
63
  const lcp = processedNavigation.timestamps.largestContentfulPaint;
64
64
  if (!lcp) {
65
- throw new LighthouseError(LighthouseError.errors.NO_LCP);
65
+ throw new LanternError('NO_LCP');
66
66
  }
67
67
 
68
68
  return FirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, {
@@ -128,6 +128,9 @@ class PageDependencyGraph {
128
128
  continue;
129
129
  }
130
130
 
131
+ /** @type {number|undefined} */
132
+ let correctedEndTs = undefined;
133
+
131
134
  // Capture all events that occurred within the task
132
135
  /** @type {Array<LH.TraceEvent>} */
133
136
  const children = [];
@@ -136,10 +139,19 @@ class PageDependencyGraph {
136
139
  i < mainThreadEvents.length && mainThreadEvents[i].ts < endTime;
137
140
  i++
138
141
  ) {
142
+ // Temporary fix for a Chrome bug where some RunTask events can be overlapping.
143
+ // We correct that here be ensuring each RunTask ends at least 1 microsecond before the next
144
+ // https://github.com/GoogleChrome/lighthouse/issues/15896
145
+ // https://issues.chromium.org/issues/329678173
146
+ if (TraceProcessor.isScheduleableTask(mainThreadEvents[i]) && mainThreadEvents[i].dur) {
147
+ correctedEndTs = mainThreadEvents[i].ts - 1;
148
+ break;
149
+ }
150
+
139
151
  children.push(mainThreadEvents[i]);
140
152
  }
141
153
 
142
- nodes.push(new CPUNode(evt, children));
154
+ nodes.push(new CPUNode(evt, children, correctedEndTs));
143
155
  }
144
156
 
145
157
  return nodes;
@@ -359,7 +371,7 @@ class PageDependencyGraph {
359
371
  isFirst = foundFirstParse = true;
360
372
  }
361
373
 
362
- if (isFirst || node.event.dur >= minimumEvtDur) {
374
+ if (isFirst || node.duration >= minimumEvtDur) {
363
375
  // Don't prune this node. The task is long / important so it will impact simulation.
364
376
  continue;
365
377
  }
@@ -276,7 +276,7 @@ class Simulator {
276
276
  ? this._layoutTaskMultiplier
277
277
  : this._cpuSlowdownMultiplier;
278
278
  const totalDuration = Math.min(
279
- Math.round(cpuNode.event.dur / 1000 * multiplier),
279
+ Math.round(cpuNode.duration / 1000 * multiplier),
280
280
  DEFAULT_MAXIMUM_CPU_TASK_DURATION
281
281
  );
282
282
  const estimatedTimeElapsed = totalDuration - timingData.timeElapsed;
@@ -76,13 +76,13 @@ const UIStrings = {
76
76
  criTimeout: 'Timeout waiting for initial Debugger Protocol connection.',
77
77
  /**
78
78
  * @description Error message explaining that a resource that was required for testing was never collected. "artifactName" will be replaced with the name of the resource that wasn't collected.
79
- * @example {WebAppManifest} artifactName
79
+ * @example {MainDocumentContent} artifactName
80
80
  * */
81
81
  missingRequiredArtifact: 'Required {artifactName} gatherer did not run.',
82
82
  /**
83
83
  * @description Error message explaining that there was an error while trying to collect a resource that was required for testing. "artifactName" will be replaced with the name of the resource that wasn't collected; "errorMessage" will be replaced with a string description of the error that occurred.
84
- * @example {WebAppManifest} artifactName
85
- * @example {Manifest invalid} errorMessage
84
+ * @example {MainDocumentContent} artifactName
85
+ * @example {Could not find main document} errorMessage
86
86
  * */
87
87
  erroredRequiredArtifact: 'Required {artifactName} gatherer encountered an error: {errorMessage}',
88
88
 
package/core/runner.js CHANGED
@@ -106,7 +106,6 @@ class Runner {
106
106
  networkUserAgent: artifacts.NetworkUserAgent,
107
107
  hostUserAgent: artifacts.HostUserAgent,
108
108
  benchmarkIndex: artifacts.BenchmarkIndex,
109
- benchmarkIndexes: artifacts.BenchmarkIndexes,
110
109
  credits,
111
110
  },
112
111
  audits: auditResultsById,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "11.7.0-dev.20240410",
4
+ "version": "11.7.0-dev.20240411",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
package/tsconfig.json CHANGED
@@ -48,7 +48,6 @@
48
48
  "core/test/gather/gatherers/devtools-log-test.js",
49
49
  "core/test/gather/gatherers/dobetterweb/optimized-images-test.js",
50
50
  "core/test/gather/gatherers/dobetterweb/response-compression-test.js",
51
- "core/test/gather/gatherers/dobetterweb/tags-blocking-first-paint-test.js",
52
51
  "core/test/gather/gatherers/full-page-screenshot-test.js",
53
52
  "core/test/gather/gatherers/global-listeners-test.js",
54
53
  "core/test/gather/gatherers/html-without-javascript-test.js",
@@ -16,6 +16,7 @@ import speedline from 'speedline-core';
16
16
  import * as CDTSourceMap from '../core/lib/cdt/generated/SourceMap.js';
17
17
  import {ArbitraryEqualityMap} from '../core/lib/arbitrary-equality-map.js';
18
18
  import type { TaskNode as _TaskNode } from '../core/lib/tracehouse/main-thread-tasks.js';
19
+ import type {EnabledHandlers} from '../core/computed/trace-engine-result.js';
19
20
  import AuditDetails from './lhr/audit-details.js'
20
21
  import Config from './config.js';
21
22
  import Gatherer from './gatherer.js';
@@ -45,8 +46,6 @@ interface UniversalBaseArtifacts {
45
46
  LighthouseRunWarnings: Array<string | IcuMessage>;
46
47
  /** The benchmark index that indicates rough device class. */
47
48
  BenchmarkIndex: number;
48
- /** Many benchmark indexes. Many. */
49
- BenchmarkIndexes?: number[];
50
49
  /** An object containing information about the testing configuration used by Lighthouse. */
51
50
  settings: Config.Settings;
52
51
  /** The timing instrumentation of the gather portion of a run. */
@@ -79,18 +78,20 @@ interface ContextualBaseArtifacts {
79
78
  interface PublicGathererArtifacts {
80
79
  /** ConsoleMessages deprecation and intervention warnings, console API calls, and exceptions logged by Chrome during page load. */
81
80
  ConsoleMessages: Artifacts.ConsoleMessage[];
82
- /** All the iframe elements in the page. */
83
- IFrameElements: Artifacts.IFrameElement[];
84
- /** The contents of the main HTML document network resource. */
85
- MainDocumentContent: string;
81
+ /** The primary log of devtools protocol activity. */
82
+ DevtoolsLog: DevtoolsLog;
86
83
  /** Information on size and loading for all the images in the page. Natural size information for `picture` and CSS images is only available if the image was one of the largest 50 images. */
87
84
  ImageElements: Artifacts.ImageElement[];
88
85
  /** All the link elements on the page or equivalently declared in `Link` headers. @see https://html.spec.whatwg.org/multipage/links.html */
89
86
  LinkElements: Artifacts.LinkElement[];
87
+ /** The contents of the main HTML document network resource. */
88
+ MainDocumentContent: string;
90
89
  /** The values of the <meta> elements in the head. */
91
90
  MetaElements: Array<{name?: string, content?: string, property?: string, httpEquiv?: string, charset?: string, node: Artifacts.NodeDetails}>;
92
91
  /** Information on all scripts in the page. */
93
92
  Scripts: Artifacts.Script[];
93
+ /** The primary trace taken over the entire run. */
94
+ Trace: Trace;
94
95
  /** The dimensions and devicePixelRatio of the loaded viewport. */
95
96
  ViewportDimensions: Artifacts.ViewportDimensions;
96
97
  }
@@ -110,8 +111,6 @@ export interface GathererArtifacts extends PublicGathererArtifacts {
110
111
  CacheContents: string[];
111
112
  /** CSS coverage information for styles used by page's final state. */
112
113
  CSSUsage: {rules: Crdp.CSS.RuleUsage[], stylesheets: Artifacts.CSSStyleSheetInfo[]};
113
- /** The primary log of devtools protocol activity. */
114
- DevtoolsLog: DevtoolsLog;
115
114
  /** The log of devtools protocol activity if there was a page load error and Chrome navigated to a `chrome-error://` page. */
116
115
  DevtoolsLogError: DevtoolsLog;
117
116
  /** Information on the document's doctype(or null if not present), specifically the name, publicId, and systemId.
@@ -121,12 +120,12 @@ export interface GathererArtifacts extends PublicGathererArtifacts {
121
120
  DOMStats: Artifacts.DOMStats;
122
121
  /** Information on poorly sized font usage and the text affected by it. */
123
122
  FontSize: Artifacts.FontSize;
123
+ /** All the iframe elements in the page. */
124
+ IFrameElements: Artifacts.IFrameElement[];
124
125
  /** All the input elements, including associated form and label elements. */
125
126
  Inputs: {inputs: Artifacts.InputElement[]; forms: Artifacts.FormElement[]; labels: Artifacts.LabelElement[]};
126
127
  /** Screenshot of the entire page (rather than just the above the fold content). */
127
128
  FullPageScreenshot: LHResult.FullPageScreenshot | null;
128
- /** Information about event listeners registered on the global object. */
129
- GlobalListeners: Array<Artifacts.GlobalListener>;
130
129
  /** The issues surfaced in the devtools Issues panel */
131
130
  InspectorIssues: Artifacts.InspectorIssues;
132
131
  /** JS coverage information for code used during audit. Keyed by script id. */
@@ -148,12 +147,6 @@ export interface GathererArtifacts extends PublicGathererArtifacts {
148
147
  SourceMaps: Array<Artifacts.SourceMap>;
149
148
  /** Information on detected tech stacks (e.g. JS libraries) used by the page. */
150
149
  Stacks: Artifacts.DetectedStack[];
151
- /** Information on <script> and <link> tags blocking first paint. */
152
- TagsBlockingFirstPaint: Artifacts.TagBlockingFirstPaint[];
153
- /** Information about tap targets including their position and size. */
154
- TapTargets: Artifacts.TapTarget[];
155
- /** The primary trace taken over the entire run. */
156
- Trace: Trace;
157
150
  /** The trace if there was a page load error and Chrome navigated to a `chrome-error://` page. */
158
151
  TraceError: Trace;
159
152
  /** Elements associated with metrics (ie: Largest Contentful Paint element). */
@@ -189,6 +182,8 @@ declare module Artifacts {
189
182
  finalDisplayedUrl: string;
190
183
  }
191
184
 
185
+ type Rect = AuditDetails.Rect;
186
+
192
187
  interface NodeDetails {
193
188
  lhId: string,
194
189
  devtoolsNodePath: string,
@@ -294,19 +289,6 @@ declare module Artifacts {
294
289
  content?: string;
295
290
  }
296
291
 
297
- interface ScriptElement {
298
- type: string | null
299
- src: string | null
300
- /** The `id` property of the script element; null if it had no `id` or if `source` is 'network'. */
301
- id: string | null
302
- async: boolean
303
- defer: boolean
304
- /** Details for node in DOM for the script element */
305
- node: NodeDetails | null
306
- /** Where the script was discovered, either in the head, the body, or network records. */
307
- source: 'head'|'body'|'network'
308
- }
309
-
310
292
  /** @see https://sourcemaps.info/spec.html#h.qz3o9nc69um5 */
311
293
  type RawSourceMap = {
312
294
  /** File version and must be a positive integer. */
@@ -519,27 +501,6 @@ declare module Artifacts {
519
501
  resourceSize: number;
520
502
  }
521
503
 
522
- interface TagBlockingFirstPaint {
523
- startTime: number;
524
- endTime: number;
525
- transferSize: number;
526
- tag: {
527
- tagName: 'LINK'|'SCRIPT';
528
- /** The value of `HTMLLinkElement.href` or `HTMLScriptElement.src`. */
529
- url: string;
530
- /** A record of when changes to the `HTMLLinkElement.media` attribute occurred and if the new media type matched the page. */
531
- mediaChanges?: Array<{href: string, media: string, msSinceHTMLEnd: number, matches: boolean}>;
532
- };
533
- }
534
-
535
- type Rect = AuditDetails.Rect;
536
-
537
- interface TapTarget {
538
- node: NodeDetails;
539
- href: string;
540
- clientRects: Rect[];
541
- }
542
-
543
504
  interface TraceElement {
544
505
  traceEventType: 'largest-contentful-paint'|'layout-shift'|'animation'|'responsiveness';
545
506
  node: NodeDetails;
@@ -548,7 +509,10 @@ declare module Artifacts {
548
509
  type?: string;
549
510
  }
550
511
 
551
- type TraceEngineResult = TraceEngine.Handlers.Types.TraceParseData;
512
+ interface TraceEngineResult {
513
+ data: TraceEngine.Handlers.Types.EnabledHandlerDataWithMeta<EnabledHandlers>;
514
+ insights: TraceEngine.Insights.Types.TraceInsightData<EnabledHandlers>;
515
+ }
552
516
 
553
517
  interface TraceEngineRootCauses {
554
518
  layoutShifts: Record<number, LayoutShiftRootCausesData>;
@@ -811,18 +775,6 @@ declare module Artifacts {
811
775
  node: NodeDetails;
812
776
  }
813
777
 
814
- /** Information about an event listener registered on the global object. */
815
- interface GlobalListener {
816
- /** Event listener type, limited to those events currently of interest. */
817
- type: 'pagehide'|'unload'|'visibilitychange';
818
- /** The DevTools protocol script identifier. */
819
- scriptId: string;
820
- /** Line number in the script (0-based). */
821
- lineNumber: number;
822
- /** Column number in the script (0-based). */
823
- columnNumber: number;
824
- }
825
-
826
778
  /** Describes a generic console message. */
827
779
  interface BaseConsoleMessage {
828
780
  /**
@@ -71,8 +71,6 @@ declare module Result {
71
71
  networkUserAgent: string;
72
72
  /** The benchmark index number that indicates rough device class. */
73
73
  benchmarkIndex: number;
74
- /** Many benchmark indexes. */
75
- benchmarkIndexes?: number[];
76
74
  /** The version of libraries with which these results were generated. Ex: axe-core. */
77
75
  credits?: Record<string, string|undefined>,
78
76
  }
@@ -1,47 +0,0 @@
1
- export default TagsBlockingFirstPaint;
2
- export type MediaChange = {
3
- href: string;
4
- media: string;
5
- msSinceHTMLEnd: number;
6
- matches: boolean;
7
- };
8
- export type LinkTag = {
9
- tagName: 'LINK';
10
- url: string;
11
- href: string;
12
- rel: string;
13
- media: string;
14
- disabled: boolean;
15
- mediaChanges: Array<MediaChange>;
16
- };
17
- export type ScriptTag = {
18
- tagName: 'SCRIPT';
19
- url: string;
20
- src: string;
21
- };
22
- declare class TagsBlockingFirstPaint extends BaseGatherer {
23
- /**
24
- * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
25
- * @return {Map<string, LH.Artifacts.NetworkRequest>}
26
- */
27
- static _filteredAndIndexedByUrl(networkRecords: Array<LH.Artifacts.NetworkRequest>): Map<string, LH.Artifacts.NetworkRequest>;
28
- /**
29
- * @param {LH.Gatherer.Driver} driver
30
- * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
31
- * @return {Promise<Array<LH.Artifacts.TagBlockingFirstPaint>>}
32
- */
33
- static findBlockingTags(driver: LH.Gatherer.Driver, networkRecords: Array<LH.Artifacts.NetworkRequest>): Promise<Array<LH.Artifacts.TagBlockingFirstPaint>>;
34
- /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
35
- meta: LH.Gatherer.GathererMeta<'DevtoolsLog'>;
36
- /**
37
- * @param {LH.Gatherer.Context} context
38
- */
39
- startSensitiveInstrumentation(context: LH.Gatherer.Context): Promise<void>;
40
- /**
41
- * @param {LH.Gatherer.Context<'DevtoolsLog'>} context
42
- * @return {Promise<LH.Artifacts['TagsBlockingFirstPaint']>}
43
- */
44
- getArtifact(context: LH.Gatherer.Context<'DevtoolsLog'>): Promise<LH.Artifacts['TagsBlockingFirstPaint']>;
45
- }
46
- import BaseGatherer from '../../base-gatherer.js';
47
- //# sourceMappingURL=tags-blocking-first-paint.d.ts.map
@@ -1,233 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2016 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- /**
7
- * @fileoverview
8
- * Identifies stylesheets, HTML Imports, and scripts that potentially block
9
- * the first paint of the page by running several scripts in the page context.
10
- * Candidate blocking tags are collected by querying for all script tags in
11
- * the head of the page and all link tags that are either matching media
12
- * stylesheets or non-async HTML imports. These are then compared to the
13
- * network requests to ensure they were initiated by the parser and not
14
- * injected with script. To avoid false positives from strategies like
15
- * (http://filamentgroup.github.io/loadCSS/test/preload.html), a separate
16
- * script is run to flag all links that at one point were rel=preload.
17
- */
18
-
19
-
20
- import {NetworkRecords} from '../../../computed/network-records.js';
21
- import DevtoolsLog from '../devtools-log.js';
22
- import BaseGatherer from '../../base-gatherer.js';
23
-
24
- /* global document, window, HTMLLinkElement, SVGScriptElement */
25
-
26
- /** @typedef {{href: string, media: string, msSinceHTMLEnd: number, matches: boolean}} MediaChange */
27
- /** @typedef {{tagName: 'LINK', url: string, href: string, rel: string, media: string, disabled: boolean, mediaChanges: Array<MediaChange>}} LinkTag */
28
- /** @typedef {{tagName: 'SCRIPT', url: string, src: string}} ScriptTag */
29
-
30
- /* c8 ignore start */
31
- function installMediaListener() {
32
- // @ts-expect-error - inserted in page to track media changes.
33
- window.___linkMediaChanges = [];
34
- Object.defineProperty(HTMLLinkElement.prototype, 'media', {
35
- set: function(val) {
36
- /** @type {MediaChange} */
37
- const mediaChange = {
38
- href: this.href,
39
- media: val,
40
- msSinceHTMLEnd: Date.now() - performance.timing.responseEnd,
41
- matches: window.matchMedia(val).matches,
42
- };
43
- // @ts-expect-error - `___linkMediaChanges` created above.
44
- window.___linkMediaChanges.push(mediaChange);
45
-
46
- this.setAttribute('media', val);
47
- },
48
- });
49
- }
50
- /* c8 ignore stop */
51
-
52
- /**
53
- * @return {Promise<Array<LinkTag | ScriptTag>>}
54
- */
55
- /* c8 ignore start */
56
- async function collectTagsThatBlockFirstPaint() {
57
- /** @type {Array<MediaChange>} */
58
- // @ts-expect-error - `___linkMediaChanges` created in `installMediaListener`.
59
- const linkMediaChanges = window.___linkMediaChanges;
60
-
61
- try {
62
- /** @type {Array<LinkTag>} */
63
- const linkTags = [...document.querySelectorAll('link')]
64
- .filter(linkTag => {
65
- // Ignore malformed links with no href (e.g. `<link rel="stylesheet" href="">`)
66
- // The resolved `linkTag.href` will be the main document, but the main document
67
- // should never be render blocking.
68
- if (!linkTag.getAttribute('href')) return false;
69
-
70
- // Filter stylesheet/HTML imports that block rendering.
71
- // https://www.igvita.com/2012/06/14/debunking-responsive-css-performance-myths/
72
- // https://www.w3.org/TR/html-imports/#dfn-import-async-attribute
73
- const blockingStylesheet = linkTag.rel === 'stylesheet' &&
74
- window.matchMedia(linkTag.media).matches && !linkTag.disabled;
75
- const blockingImport = linkTag.rel === 'import' && !linkTag.hasAttribute('async');
76
- return blockingStylesheet || blockingImport;
77
- })
78
- .map(tag => {
79
- return {
80
- tagName: 'LINK',
81
- url: tag.href,
82
- href: tag.href,
83
- rel: tag.rel,
84
- media: tag.media,
85
- disabled: tag.disabled,
86
- mediaChanges: linkMediaChanges.filter(item => item.href === tag.href),
87
- };
88
- });
89
-
90
- /** @type {Array<ScriptTag>} */
91
- const scriptTags = [...document.querySelectorAll('head script[src]')]
92
- .filter(/** @return {scriptTag is HTMLScriptElement} */ scriptTag => {
93
- // SVGScriptElement can't appear in <head> (it'll be kicked to <body>), but keep tsc happy.
94
- // https://html.spec.whatwg.org/multipage/semantics.html#the-head-element
95
- if (scriptTag instanceof SVGScriptElement) return false;
96
-
97
- return (
98
- !scriptTag.hasAttribute('async') &&
99
- !scriptTag.hasAttribute('defer') &&
100
- !/^data:/.test(scriptTag.src) &&
101
- !/^blob:/.test(scriptTag.src) &&
102
- scriptTag.getAttribute('type') !== 'module'
103
- );
104
- })
105
- .map(tag => {
106
- return {
107
- tagName: 'SCRIPT',
108
- url: tag.src,
109
- src: tag.src,
110
- };
111
- });
112
-
113
- return [...linkTags, ...scriptTags];
114
- } catch (e) {
115
- const friendly = 'Unable to gather Scripts/Stylesheets/HTML Imports on the page';
116
- throw new Error(`${friendly}: ${e.message}`);
117
- }
118
- }
119
- /* c8 ignore stop */
120
-
121
- class TagsBlockingFirstPaint extends BaseGatherer {
122
- /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
123
- meta = {
124
- supportedModes: ['navigation'],
125
- dependencies: {DevtoolsLog: DevtoolsLog.symbol},
126
- };
127
-
128
- /**
129
- * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
130
- * @return {Map<string, LH.Artifacts.NetworkRequest>}
131
- */
132
- static _filteredAndIndexedByUrl(networkRecords) {
133
- /** @type {Map<string, LH.Artifacts.NetworkRequest>} */
134
- const result = new Map();
135
-
136
- for (const record of networkRecords) {
137
- if (!record.finished) continue;
138
-
139
- const isParserGenerated = record.initiator.type === 'parser';
140
- // A stylesheet only blocks script if it was initiated by the parser
141
- // https://html.spec.whatwg.org/multipage/semantics.html#interactions-of-styling-and-scripting
142
- const isParserScriptOrStyle = /(css|script)/.test(record.mimeType) && isParserGenerated;
143
- const isFailedRequest = record.failed;
144
- const isHtml = record.mimeType && record.mimeType.includes('html');
145
-
146
- // Filter stylesheet, javascript, and html import mimetypes.
147
- // Include 404 scripts/links generated by the parser because they are likely blocking.
148
- if (isHtml || isParserScriptOrStyle || (isFailedRequest && isParserGenerated)) {
149
- result.set(record.url, record);
150
- }
151
- }
152
-
153
- return result;
154
- }
155
-
156
- /**
157
- * @param {LH.Gatherer.Driver} driver
158
- * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
159
- * @return {Promise<Array<LH.Artifacts.TagBlockingFirstPaint>>}
160
- */
161
- static async findBlockingTags(driver, networkRecords) {
162
- const firstRequestEndTime = networkRecords.reduce(
163
- (min, record) => Math.min(min, record.networkEndTime),
164
- Infinity
165
- );
166
- const tags = await driver.executionContext.evaluate(collectTagsThatBlockFirstPaint, {args: []});
167
- const requests = TagsBlockingFirstPaint._filteredAndIndexedByUrl(networkRecords);
168
-
169
- /** @type {Array<LH.Artifacts.TagBlockingFirstPaint>} */
170
- const result = [];
171
- for (const tag of tags) {
172
- const request = requests.get(tag.url);
173
- if (!request || request.isLinkPreload) continue;
174
-
175
- let endTime = request.networkEndTime;
176
- let mediaChanges;
177
-
178
- if (tag.tagName === 'LINK') {
179
- // Even if the request was initially blocking or appeared to be blocking once the
180
- // page was loaded, the media attribute could have been changed during load, capping the
181
- // amount of time it was render blocking. See https://github.com/GoogleChrome/lighthouse/issues/2832.
182
- const timesResourceBecameNonBlocking = tag.mediaChanges
183
- .filter(change => !change.matches)
184
- .map(change => change.msSinceHTMLEnd);
185
- if (timesResourceBecameNonBlocking.length > 0) {
186
- const earliestNonBlockingTime = Math.min(...timesResourceBecameNonBlocking);
187
- const lastTimeResourceWasBlocking = Math.max(
188
- request.networkRequestTime,
189
- firstRequestEndTime + earliestNonBlockingTime / 1000
190
- );
191
- endTime = Math.min(endTime, lastTimeResourceWasBlocking);
192
- }
193
-
194
- mediaChanges = tag.mediaChanges;
195
- }
196
-
197
- const {tagName, url} = tag;
198
-
199
- result.push({
200
- tag: {tagName, url, mediaChanges},
201
- transferSize: request.transferSize,
202
- startTime: request.networkRequestTime,
203
- endTime,
204
- });
205
-
206
- // Prevent duplicates from showing up again
207
- requests.delete(tag.url);
208
- }
209
-
210
- return result;
211
- }
212
-
213
- /**
214
- * @param {LH.Gatherer.Context} context
215
- */
216
- async startSensitiveInstrumentation(context) {
217
- const {executionContext} = context.driver;
218
- // Don't return return value of `evaluateOnNewDocument`.
219
- await executionContext.evaluateOnNewDocument(installMediaListener, {args: []});
220
- }
221
-
222
- /**
223
- * @param {LH.Gatherer.Context<'DevtoolsLog'>} context
224
- * @return {Promise<LH.Artifacts['TagsBlockingFirstPaint']>}
225
- */
226
- async getArtifact(context) {
227
- const devtoolsLog = context.dependencies.DevtoolsLog;
228
- const networkRecords = await NetworkRecords.request(devtoolsLog, context);
229
- return TagsBlockingFirstPaint.findBlockingTags(context.driver, networkRecords);
230
- }
231
- }
232
-
233
- export default TagsBlockingFirstPaint;