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.
- package/core/audits/byte-efficiency/render-blocking-resources.js +25 -26
- package/core/audits/layout-shifts.js +1 -1
- package/core/computed/metrics/lantern-first-contentful-paint.js +3 -2
- package/core/computed/metrics/lantern-first-meaningful-paint.js +3 -2
- package/core/computed/metrics/lantern-interactive.js +3 -2
- package/core/computed/metrics/lantern-largest-contentful-paint.js +3 -2
- package/core/computed/metrics/lantern-max-potential-fid.js +3 -2
- package/core/computed/metrics/lantern-metric.d.ts +5 -0
- package/core/computed/metrics/lantern-metric.js +23 -1
- package/core/computed/metrics/lantern-speed-index.js +3 -2
- package/core/computed/navigation-insights.d.ts +36 -0
- package/core/computed/navigation-insights.js +35 -0
- package/core/computed/trace-engine-result.d.ts +16 -3
- package/core/computed/trace-engine-result.js +19 -14
- package/core/config/default-config.js +0 -1
- package/core/config/filters.js +0 -1
- package/core/gather/gatherers/root-causes.d.ts +2 -2
- package/core/gather/gatherers/root-causes.js +6 -5
- package/core/gather/gatherers/trace-elements.d.ts +2 -2
- package/core/gather/gatherers/trace-elements.js +2 -2
- package/core/lib/lantern/cpu-node.d.ts +7 -1
- package/core/lib/lantern/cpu-node.js +12 -2
- package/core/lib/lantern/lantern-error.d.ts +8 -0
- package/core/lib/lantern/lantern-error.js +9 -0
- package/core/lib/lantern/metrics/first-meaningful-paint.js +2 -4
- package/core/lib/lantern/metrics/interactive.js +1 -1
- package/core/lib/lantern/metrics/largest-contentful-paint.js +3 -3
- package/core/lib/lantern/page-dependency-graph.js +14 -2
- package/core/lib/lantern/simulator/simulator.js +1 -1
- package/core/lib/lh-error.js +3 -3
- package/core/runner.js +0 -1
- package/package.json +1 -1
- package/tsconfig.json +0 -1
- package/types/artifacts.d.ts +15 -63
- package/types/lhr/lhr.d.ts +0 -2
- package/core/gather/gatherers/dobetterweb/tags-blocking-first-paint.d.ts +0 -47
- 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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
279
|
+
Math.round(cpuNode.duration / 1000 * multiplier),
|
|
280
280
|
DEFAULT_MAXIMUM_CPU_TASK_DURATION
|
|
281
281
|
);
|
|
282
282
|
const estimatedTimeElapsed = totalDuration - timingData.timeElapsed;
|
package/core/lib/lh-error.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
85
|
-
* @example {
|
|
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
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",
|
package/types/artifacts.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
83
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/types/lhr/lhr.d.ts
CHANGED
|
@@ -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;
|