lighthouse 10.3.0 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/test/smokehouse/frontends/smokehouse-bin.js +19 -13
- package/cli/test/smokehouse/lighthouse-runners/bundle.js +3 -5
- package/core/audits/accessibility/aria-dialog-name.d.ts +10 -0
- package/core/audits/accessibility/aria-dialog-name.js +45 -0
- package/core/audits/accessibility/aria-text.d.ts +10 -0
- package/core/audits/accessibility/aria-text.js +44 -0
- package/core/audits/accessibility/empty-heading.d.ts +10 -0
- package/core/audits/accessibility/empty-heading.js +45 -0
- package/core/audits/accessibility/identical-links-same-purpose.d.ts +10 -0
- package/core/audits/accessibility/identical-links-same-purpose.js +45 -0
- package/core/audits/accessibility/landmark-one-main.d.ts +10 -0
- package/core/audits/accessibility/landmark-one-main.js +44 -0
- package/core/audits/accessibility/link-in-text-block.d.ts +10 -0
- package/core/audits/accessibility/link-in-text-block.js +44 -0
- package/core/audits/accessibility/select-name.d.ts +10 -0
- package/core/audits/accessibility/select-name.js +44 -0
- package/core/audits/accessibility/target-size.d.ts +10 -0
- package/core/audits/accessibility/target-size.js +45 -0
- package/core/audits/audit.d.ts +6 -0
- package/core/audits/audit.js +12 -0
- package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +22 -6
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +97 -25
- package/core/audits/long-tasks.d.ts +48 -0
- package/core/audits/long-tasks.js +146 -19
- package/core/audits/non-composited-animations.js +7 -0
- package/core/audits/prioritize-lcp-image.js +2 -1
- package/core/audits/redirects.js +4 -0
- package/core/audits/seo/link-text.js +10 -0
- package/core/audits/unsized-images.js +3 -0
- package/core/audits/viewport.js +11 -0
- package/core/audits/work-during-interaction.js +13 -2
- package/core/computed/js-bundles.js +1 -1
- package/core/computed/metrics/tbt-utils.d.ts +26 -0
- package/core/computed/metrics/tbt-utils.js +48 -28
- package/core/computed/metrics/total-blocking-time.js +1 -1
- package/core/computed/tbt-impact-tasks.d.ts +54 -0
- package/core/computed/tbt-impact-tasks.js +221 -0
- package/core/config/default-config.js +17 -0
- package/core/config/filters.d.ts +9 -9
- package/core/config/filters.js +7 -7
- package/core/config/validation.js +12 -0
- package/core/gather/base-gatherer.d.ts +1 -3
- package/core/gather/base-gatherer.js +1 -3
- package/core/gather/gatherers/accessibility.js +8 -3
- package/core/gather/gatherers/seo/font-size.d.ts +0 -1
- package/core/gather/gatherers/seo/font-size.js +0 -1
- package/core/gather/gatherers/source-maps.js +3 -2
- package/core/lib/cdt/SDK.d.ts +1 -1
- package/core/lib/cdt/SDK.js +2 -2
- package/core/lib/dependency-graph/simulator/simulator.js +3 -1
- package/core/lib/lh-error.d.ts +14 -2
- package/core/lib/lh-error.js +7 -1
- package/core/lib/navigation-error.d.ts +2 -2
- package/core/lib/navigation-error.js +1 -1
- package/core/lib/network-recorder.js +1 -0
- package/core/lib/stack-packs.js +4 -0
- package/dist/report/bundle.esm.js +2 -1
- package/dist/report/flow.js +9 -9
- package/dist/report/standalone.js +2 -2
- package/package.json +5 -5
- package/report/assets/styles.css +5 -1
- package/report/renderer/category-renderer.js +1 -0
- package/report/renderer/components.js +1 -1
- package/shared/localization/locales/ar-XB.json +233 -2
- package/shared/localization/locales/ar.json +233 -2
- package/shared/localization/locales/bg.json +233 -2
- package/shared/localization/locales/ca.json +233 -2
- package/shared/localization/locales/cs.json +246 -15
- package/shared/localization/locales/da.json +239 -8
- package/shared/localization/locales/de.json +238 -7
- package/shared/localization/locales/el.json +234 -3
- package/shared/localization/locales/en-GB.json +233 -2
- package/shared/localization/locales/en-US.json +87 -0
- package/shared/localization/locales/en-XA.json +233 -2
- package/shared/localization/locales/en-XL.json +87 -0
- package/shared/localization/locales/es-419.json +233 -2
- package/shared/localization/locales/es.json +233 -2
- package/shared/localization/locales/fi.json +233 -2
- package/shared/localization/locales/fil.json +236 -5
- package/shared/localization/locales/fr.json +235 -4
- package/shared/localization/locales/he.json +233 -2
- package/shared/localization/locales/hi.json +236 -5
- package/shared/localization/locales/hr.json +234 -3
- package/shared/localization/locales/hu.json +233 -2
- package/shared/localization/locales/id.json +233 -2
- package/shared/localization/locales/it.json +234 -3
- package/shared/localization/locales/ja.json +233 -2
- package/shared/localization/locales/ko.json +233 -2
- package/shared/localization/locales/lt.json +233 -2
- package/shared/localization/locales/lv.json +250 -19
- package/shared/localization/locales/nl.json +233 -2
- package/shared/localization/locales/no.json +235 -4
- package/shared/localization/locales/pl.json +233 -2
- package/shared/localization/locales/pt-PT.json +233 -2
- package/shared/localization/locales/pt.json +234 -3
- package/shared/localization/locales/ro.json +233 -2
- package/shared/localization/locales/ru.json +233 -2
- package/shared/localization/locales/sk.json +233 -2
- package/shared/localization/locales/sl.json +233 -2
- package/shared/localization/locales/sr-Latn.json +233 -2
- package/shared/localization/locales/sr.json +233 -2
- package/shared/localization/locales/sv.json +233 -2
- package/shared/localization/locales/ta.json +233 -2
- package/shared/localization/locales/te.json +233 -2
- package/shared/localization/locales/th.json +233 -2
- package/shared/localization/locales/tr.json +233 -2
- package/shared/localization/locales/uk.json +233 -2
- package/shared/localization/locales/vi.json +233 -2
- package/shared/localization/locales/zh-HK.json +233 -2
- package/shared/localization/locales/zh-TW.json +235 -4
- package/shared/localization/locales/zh.json +239 -8
- package/tsconfig.json +1 -1
- package/types/artifacts.d.ts +9 -2
|
@@ -10,6 +10,9 @@ import * as i18n from '../../lib/i18n/i18n.js';
|
|
|
10
10
|
import {NetworkRecords} from '../../computed/network-records.js';
|
|
11
11
|
import {LoadSimulator} from '../../computed/load-simulator.js';
|
|
12
12
|
import {PageDependencyGraph} from '../../computed/page-dependency-graph.js';
|
|
13
|
+
import {LanternLargestContentfulPaint} from '../../computed/metrics/lantern-largest-contentful-paint.js';
|
|
14
|
+
import {LanternFirstContentfulPaint} from '../../computed/metrics/lantern-first-contentful-paint.js';
|
|
15
|
+
import {LCPImageRecord} from '../../computed/lcp-image-record.js';
|
|
13
16
|
|
|
14
17
|
const str_ = i18n.createIcuMessageFn(import.meta.url, {});
|
|
15
18
|
|
|
@@ -104,9 +107,7 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
104
107
|
*/
|
|
105
108
|
static async audit(artifacts, context) {
|
|
106
109
|
const gatherContext = artifacts.GatherContext;
|
|
107
|
-
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
108
110
|
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
109
|
-
const URL = artifacts.URL;
|
|
110
111
|
const settings = context?.settings || {};
|
|
111
112
|
const simulatorOptions = {
|
|
112
113
|
devtoolsLog,
|
|
@@ -125,34 +126,29 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
const
|
|
129
|
+
const metricComputationInput = Audit.makeMetricComputationDataInput(artifacts, context);
|
|
130
|
+
|
|
131
|
+
const [result, simulator] = await Promise.all([
|
|
129
132
|
this.audit_(artifacts, networkRecords, context),
|
|
130
|
-
// Page dependency graph is only used in navigation mode.
|
|
131
|
-
gatherContext.gatherMode === 'navigation' ?
|
|
132
|
-
PageDependencyGraph.request({trace, devtoolsLog, URL}, context) :
|
|
133
|
-
null,
|
|
134
133
|
LoadSimulator.request(simulatorOptions, context),
|
|
135
134
|
]);
|
|
136
135
|
|
|
137
|
-
return this.createAuditProduct(result,
|
|
136
|
+
return this.createAuditProduct(result, simulator, metricComputationInput, context);
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
/**
|
|
141
|
-
* Computes the estimated effect of all the byte savings on the
|
|
142
|
-
*
|
|
143
|
-
* - end time of the last long task in the provided graph
|
|
144
|
-
* - (if includeLoad is true or not provided) end time of the last node in the graph
|
|
140
|
+
* Computes the estimated effect of all the byte savings on the provided graph.
|
|
145
141
|
*
|
|
146
142
|
* @param {Array<LH.Audit.ByteEfficiencyItem>} results The array of byte savings results per resource
|
|
147
143
|
* @param {Node} graph
|
|
148
144
|
* @param {Simulator} simulator
|
|
149
|
-
* @param {{
|
|
150
|
-
* @return {number}
|
|
145
|
+
* @param {{label?: string, providedWastedBytesByUrl?: Map<string, number>}=} options
|
|
146
|
+
* @return {{savings: number, simulationBeforeChanges: LH.Gatherer.Simulation.Result, simulationAfterChanges: LH.Gatherer.Simulation.Result}}
|
|
151
147
|
*/
|
|
152
|
-
static
|
|
153
|
-
options = Object.assign({
|
|
154
|
-
const beforeLabel = `${options.label}-before`;
|
|
155
|
-
const afterLabel = `${options.label}-after`;
|
|
148
|
+
static computeWasteWithGraph(results, graph, simulator, options) {
|
|
149
|
+
options = Object.assign({label: ''}, options);
|
|
150
|
+
const beforeLabel = `${this.meta.id}-${options.label}-before`;
|
|
151
|
+
const afterLabel = `${this.meta.id}-${options.label}-after`;
|
|
156
152
|
|
|
157
153
|
const simulationBeforeChanges = simulator.simulate(graph, {label: beforeLabel});
|
|
158
154
|
|
|
@@ -187,7 +183,36 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
187
183
|
node.record.transferSize = originalTransferSize;
|
|
188
184
|
});
|
|
189
185
|
|
|
190
|
-
const
|
|
186
|
+
const savings = simulationBeforeChanges.timeInMs - simulationAfterChanges.timeInMs;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
// Round waste to nearest 10ms
|
|
190
|
+
savings: Math.round(Math.max(savings, 0) / 10) * 10,
|
|
191
|
+
simulationBeforeChanges,
|
|
192
|
+
simulationAfterChanges,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Computes the estimated effect of all the byte savings on the maximum of the following:
|
|
198
|
+
*
|
|
199
|
+
* - end time of the last long task in the provided graph
|
|
200
|
+
* - (if includeLoad is true or not provided) end time of the last node in the graph
|
|
201
|
+
*
|
|
202
|
+
* @param {Array<LH.Audit.ByteEfficiencyItem>} results The array of byte savings results per resource
|
|
203
|
+
* @param {Node} graph
|
|
204
|
+
* @param {Simulator} simulator
|
|
205
|
+
* @param {{includeLoad?: boolean, providedWastedBytesByUrl?: Map<string, number>}=} options
|
|
206
|
+
* @return {number}
|
|
207
|
+
*/
|
|
208
|
+
static computeWasteWithTTIGraph(results, graph, simulator, options) {
|
|
209
|
+
options = Object.assign({includeLoad: true}, options);
|
|
210
|
+
const {savings: savingsOnOverallLoad, simulationBeforeChanges, simulationAfterChanges} =
|
|
211
|
+
this.computeWasteWithGraph(results, graph, simulator, {
|
|
212
|
+
...options,
|
|
213
|
+
label: 'overallLoad',
|
|
214
|
+
});
|
|
215
|
+
|
|
191
216
|
const savingsOnTTI =
|
|
192
217
|
LanternInteractive.getLastLongTaskEndTime(simulationBeforeChanges.nodeTimings) -
|
|
193
218
|
LanternInteractive.getLastLongTaskEndTime(simulationAfterChanges.nodeTimings);
|
|
@@ -201,24 +226,63 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
201
226
|
|
|
202
227
|
/**
|
|
203
228
|
* @param {ByteEfficiencyProduct} result
|
|
204
|
-
* @param {Node|null} graph
|
|
205
229
|
* @param {Simulator} simulator
|
|
206
|
-
* @param {LH.Artifacts
|
|
207
|
-
* @
|
|
230
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationInput
|
|
231
|
+
* @param {LH.Audit.Context} context
|
|
232
|
+
* @return {Promise<LH.Audit.Product>}
|
|
208
233
|
*/
|
|
209
|
-
static createAuditProduct(result,
|
|
234
|
+
static async createAuditProduct(result, simulator, metricComputationInput, context) {
|
|
210
235
|
const results = result.items.sort((itemA, itemB) => itemB.wastedBytes - itemA.wastedBytes);
|
|
211
236
|
|
|
212
237
|
const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);
|
|
213
238
|
|
|
239
|
+
/** @type {LH.Audit.MetricSavings} */
|
|
240
|
+
const metricSavings = {
|
|
241
|
+
FCP: 0,
|
|
242
|
+
LCP: 0,
|
|
243
|
+
};
|
|
244
|
+
|
|
214
245
|
// `wastedMs` may be negative, if making the opportunity change could be detrimental.
|
|
215
246
|
// This is useful information in the LHR and should be preserved.
|
|
216
247
|
let wastedMs;
|
|
217
|
-
if (gatherContext.gatherMode === 'navigation') {
|
|
218
|
-
|
|
248
|
+
if (metricComputationInput.gatherContext.gatherMode === 'navigation') {
|
|
249
|
+
const graph = await PageDependencyGraph.request(metricComputationInput, context);
|
|
250
|
+
const {
|
|
251
|
+
pessimisticGraph: pessimisticFCPGraph,
|
|
252
|
+
} = await LanternFirstContentfulPaint.request(metricComputationInput, context);
|
|
253
|
+
const {
|
|
254
|
+
pessimisticGraph: pessimisticLCPGraph,
|
|
255
|
+
} = await LanternLargestContentfulPaint.request(metricComputationInput, context);
|
|
256
|
+
|
|
219
257
|
wastedMs = this.computeWasteWithTTIGraph(results, graph, simulator, {
|
|
220
258
|
providedWastedBytesByUrl: result.wastedBytesByUrl,
|
|
221
259
|
});
|
|
260
|
+
|
|
261
|
+
const {savings: fcpSavings} = this.computeWasteWithGraph(
|
|
262
|
+
results,
|
|
263
|
+
pessimisticFCPGraph,
|
|
264
|
+
simulator,
|
|
265
|
+
{providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'fcp'}
|
|
266
|
+
);
|
|
267
|
+
const {savings: lcpGraphSavings} = this.computeWasteWithGraph(
|
|
268
|
+
results,
|
|
269
|
+
pessimisticLCPGraph,
|
|
270
|
+
simulator,
|
|
271
|
+
{providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'lcp'}
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// The LCP graph can underestimate the LCP savings if there is potential savings on the LCP record itself.
|
|
275
|
+
let lcpRecordSavings = 0;
|
|
276
|
+
const lcpRecord = await LCPImageRecord.request(metricComputationInput, context);
|
|
277
|
+
if (lcpRecord) {
|
|
278
|
+
const lcpResult = results.find(result => result.url === lcpRecord.url);
|
|
279
|
+
if (lcpResult) {
|
|
280
|
+
lcpRecordSavings = simulator.computeWastedMsFromWastedBytes(lcpResult.wastedBytes);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
metricSavings.FCP = fcpSavings;
|
|
285
|
+
metricSavings.LCP = Math.max(lcpGraphSavings, lcpRecordSavings);
|
|
222
286
|
} else {
|
|
223
287
|
wastedMs = simulator.computeWastedMsFromWastedBytes(wastedBytes);
|
|
224
288
|
}
|
|
@@ -232,6 +296,13 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
232
296
|
const details = Audit.makeOpportunityDetails(result.headings, results,
|
|
233
297
|
{overallSavingsMs: wastedMs, overallSavingsBytes: wastedBytes, sortedBy});
|
|
234
298
|
|
|
299
|
+
// TODO: Remove from debug data once `metricSavings` is added to the LHR.
|
|
300
|
+
// For now, add it to debug data for visibility.
|
|
301
|
+
details.debugData = {
|
|
302
|
+
type: 'debugdata',
|
|
303
|
+
metricSavings,
|
|
304
|
+
};
|
|
305
|
+
|
|
235
306
|
return {
|
|
236
307
|
explanation: result.explanation,
|
|
237
308
|
warnings: result.warnings,
|
|
@@ -240,6 +311,7 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
240
311
|
numericUnit: 'millisecond',
|
|
241
312
|
score: ByteEfficiencyAudit.scoreForWastedMs(wastedMs),
|
|
242
313
|
details,
|
|
314
|
+
metricSavings,
|
|
243
315
|
};
|
|
244
316
|
}
|
|
245
317
|
|
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
export default LongTasks;
|
|
2
|
+
export type TaskGroupIds = import('../lib/tracehouse/task-groups.js').TaskGroupIds;
|
|
3
|
+
export type Timing = {
|
|
4
|
+
startTime: number;
|
|
5
|
+
duration: number;
|
|
6
|
+
};
|
|
7
|
+
export type DebugTask = Timing & {
|
|
8
|
+
[p: string]: number;
|
|
9
|
+
urlIndex: number;
|
|
10
|
+
};
|
|
11
|
+
/** @typedef {import('../lib/tracehouse/task-groups.js').TaskGroupIds} TaskGroupIds */
|
|
12
|
+
/** @typedef {{startTime: number, duration: number}} Timing */
|
|
13
|
+
/** @typedef {Timing & {urlIndex: number, [p: string]: number}} DebugTask */
|
|
2
14
|
declare class LongTasks extends Audit {
|
|
15
|
+
/**
|
|
16
|
+
* Returns the timing information for the given task, recursively walking the
|
|
17
|
+
* task's children and adding up time spent in each type of task activity.
|
|
18
|
+
* If `taskTimingsByEvent` is present, it will be used for task timing instead
|
|
19
|
+
* of the timings on the tasks themselves.
|
|
20
|
+
* If `timeByTaskGroup` is not provided, a new Map will be populated with
|
|
21
|
+
* timing breakdown; if one is provided, timing breakdown will be added to the
|
|
22
|
+
* existing breakdown.
|
|
23
|
+
*
|
|
24
|
+
* TODO: when simulated, a significant number of child tasks are dropped, so
|
|
25
|
+
* most time will be attributed to 'other' (the category of the top-level
|
|
26
|
+
* RunTask). See pruning in `PageDependencyGraph.linkCPUNodes`.
|
|
27
|
+
* @param {LH.Artifacts.TaskNode} task
|
|
28
|
+
* @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
|
|
29
|
+
* @param {Map<TaskGroupIds, number>} [timeByTaskGroup]
|
|
30
|
+
* @return {{startTime: number, duration: number, timeByTaskGroup: Map<TaskGroupIds, number>}}
|
|
31
|
+
*/
|
|
32
|
+
static getTimingBreakdown(task: LH.Artifacts.TaskNode, taskTimingsByEvent: Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming> | undefined, timeByTaskGroup?: Map<import("../lib/tracehouse/task-groups.js").TaskGroupIds, number> | undefined): {
|
|
33
|
+
startTime: number;
|
|
34
|
+
duration: number;
|
|
35
|
+
timeByTaskGroup: Map<TaskGroupIds, number>;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* @param {Array<LH.Artifacts.TaskNode>} longTasks
|
|
39
|
+
* @param {Set<string>} jsUrls
|
|
40
|
+
* @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
|
|
41
|
+
* @return {LH.Audit.Details.DebugData}
|
|
42
|
+
*/
|
|
43
|
+
static makeDebugData(longTasks: Array<LH.Artifacts.TaskNode>, jsUrls: Set<string>, taskTimingsByEvent: Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming> | undefined): LH.Audit.Details.DebugData;
|
|
44
|
+
/**
|
|
45
|
+
* Get timing from task, overridden by taskTimingsByEvent if provided.
|
|
46
|
+
* @param {LH.Artifacts.TaskNode} task
|
|
47
|
+
* @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
|
|
48
|
+
* @return {Timing}
|
|
49
|
+
*/
|
|
50
|
+
static getTiming(task: LH.Artifacts.TaskNode, taskTimingsByEvent: Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming> | undefined): Timing;
|
|
3
51
|
/**
|
|
4
52
|
* @param {LH.Artifacts} artifacts
|
|
5
53
|
* @param {LH.Audit.Context} context
|
|
@@ -14,6 +14,7 @@ import {getJavaScriptURLs, getAttributableURLForTask} from '../lib/tracehouse/ta
|
|
|
14
14
|
|
|
15
15
|
/** We don't always have timing data for short tasks, if we're missing timing data. Treat it as though it were 0ms. */
|
|
16
16
|
const DEFAULT_TIMING = {startTime: 0, endTime: 0, duration: 0};
|
|
17
|
+
const DISPLAYED_TASK_COUNT = 20;
|
|
17
18
|
|
|
18
19
|
const UIStrings = {
|
|
19
20
|
/** Title of a diagnostic LH audit that provides details on the longest running tasks that occur when the page loads. */
|
|
@@ -31,6 +32,30 @@ const UIStrings = {
|
|
|
31
32
|
|
|
32
33
|
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Insert `url` into `urls` array if not already present. Returns
|
|
37
|
+
* the index of `url` in `urls` for later lookup.
|
|
38
|
+
* @param {Array<string>} urls
|
|
39
|
+
* @param {string} url
|
|
40
|
+
*/
|
|
41
|
+
function insertUrl(urls, url) {
|
|
42
|
+
const index = urls.indexOf(url);
|
|
43
|
+
if (index > -1) return index;
|
|
44
|
+
return urls.push(url) - 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {number} value
|
|
49
|
+
* @return {number}
|
|
50
|
+
*/
|
|
51
|
+
function roundTenths(value) {
|
|
52
|
+
return Math.round(value * 10) / 10;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @typedef {import('../lib/tracehouse/task-groups.js').TaskGroupIds} TaskGroupIds */
|
|
56
|
+
/** @typedef {{startTime: number, duration: number}} Timing */
|
|
57
|
+
/** @typedef {Timing & {urlIndex: number, [p: string]: number}} DebugTask */
|
|
58
|
+
|
|
34
59
|
class LongTasks extends Audit {
|
|
35
60
|
/**
|
|
36
61
|
* @return {LH.Audit.Meta}
|
|
@@ -45,6 +70,102 @@ class LongTasks extends Audit {
|
|
|
45
70
|
};
|
|
46
71
|
}
|
|
47
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Returns the timing information for the given task, recursively walking the
|
|
75
|
+
* task's children and adding up time spent in each type of task activity.
|
|
76
|
+
* If `taskTimingsByEvent` is present, it will be used for task timing instead
|
|
77
|
+
* of the timings on the tasks themselves.
|
|
78
|
+
* If `timeByTaskGroup` is not provided, a new Map will be populated with
|
|
79
|
+
* timing breakdown; if one is provided, timing breakdown will be added to the
|
|
80
|
+
* existing breakdown.
|
|
81
|
+
*
|
|
82
|
+
* TODO: when simulated, a significant number of child tasks are dropped, so
|
|
83
|
+
* most time will be attributed to 'other' (the category of the top-level
|
|
84
|
+
* RunTask). See pruning in `PageDependencyGraph.linkCPUNodes`.
|
|
85
|
+
* @param {LH.Artifacts.TaskNode} task
|
|
86
|
+
* @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
|
|
87
|
+
* @param {Map<TaskGroupIds, number>} [timeByTaskGroup]
|
|
88
|
+
* @return {{startTime: number, duration: number, timeByTaskGroup: Map<TaskGroupIds, number>}}
|
|
89
|
+
*/
|
|
90
|
+
static getTimingBreakdown(task, taskTimingsByEvent, timeByTaskGroup = new Map()) {
|
|
91
|
+
const taskTiming = LongTasks.getTiming(task, taskTimingsByEvent);
|
|
92
|
+
|
|
93
|
+
// Add up child time, while recursively stepping in to accumulate group times.
|
|
94
|
+
let childrenTime = 0;
|
|
95
|
+
if (taskTiming.duration > 0) {
|
|
96
|
+
for (const child of task.children) {
|
|
97
|
+
const {duration} = LongTasks.getTimingBreakdown(child, taskTimingsByEvent, timeByTaskGroup);
|
|
98
|
+
childrenTime += duration;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add this task's selfTime to its group's total time.
|
|
103
|
+
const selfTime = taskTiming.duration - childrenTime;
|
|
104
|
+
const taskGroupTime = timeByTaskGroup.get(task.group.id) || 0;
|
|
105
|
+
timeByTaskGroup.set(task.group.id, taskGroupTime + selfTime);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
startTime: taskTiming.startTime,
|
|
109
|
+
duration: taskTiming.duration,
|
|
110
|
+
timeByTaskGroup,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {Array<LH.Artifacts.TaskNode>} longTasks
|
|
116
|
+
* @param {Set<string>} jsUrls
|
|
117
|
+
* @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
|
|
118
|
+
* @return {LH.Audit.Details.DebugData}
|
|
119
|
+
*/
|
|
120
|
+
static makeDebugData(longTasks, jsUrls, taskTimingsByEvent) {
|
|
121
|
+
/** @type {Array<string>} */
|
|
122
|
+
const urls = [];
|
|
123
|
+
/** @type {Array<DebugTask>} */
|
|
124
|
+
const tasks = [];
|
|
125
|
+
|
|
126
|
+
for (const longTask of longTasks) {
|
|
127
|
+
const attributableUrl = getAttributableURLForTask(longTask, jsUrls);
|
|
128
|
+
|
|
129
|
+
const {startTime, duration, timeByTaskGroup} =
|
|
130
|
+
LongTasks.getTimingBreakdown(longTask, taskTimingsByEvent);
|
|
131
|
+
|
|
132
|
+
// Round time per group and sort entries so order is consistent.
|
|
133
|
+
const timeByTaskGroupEntries = [...timeByTaskGroup]
|
|
134
|
+
.map(/** @return {[TaskGroupIds, number]} */ ([group, time]) => [group, roundTenths(time)])
|
|
135
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
136
|
+
|
|
137
|
+
tasks.push({
|
|
138
|
+
urlIndex: insertUrl(urls, attributableUrl),
|
|
139
|
+
startTime: roundTenths(startTime),
|
|
140
|
+
duration: roundTenths(duration),
|
|
141
|
+
...Object.fromEntries(timeByTaskGroupEntries),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
type: 'debugdata',
|
|
147
|
+
urls,
|
|
148
|
+
tasks,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get timing from task, overridden by taskTimingsByEvent if provided.
|
|
154
|
+
* @param {LH.Artifacts.TaskNode} task
|
|
155
|
+
* @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
|
|
156
|
+
* @return {Timing}
|
|
157
|
+
*/
|
|
158
|
+
static getTiming(task, taskTimingsByEvent) {
|
|
159
|
+
/** @type {Timing} */
|
|
160
|
+
let timing = task;
|
|
161
|
+
if (taskTimingsByEvent) {
|
|
162
|
+
timing = taskTimingsByEvent.get(task.event) || DEFAULT_TIMING;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const {duration, startTime} = timing;
|
|
166
|
+
return {duration, startTime};
|
|
167
|
+
}
|
|
168
|
+
|
|
48
169
|
/**
|
|
49
170
|
* @param {LH.Artifacts} artifacts
|
|
50
171
|
* @param {LH.Audit.Context} context
|
|
@@ -58,10 +179,12 @@ class LongTasks extends Audit {
|
|
|
58
179
|
const devtoolsLog = artifacts.devtoolsLogs[LongTasks.DEFAULT_PASS];
|
|
59
180
|
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
|
|
60
181
|
|
|
61
|
-
/** @type {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming
|
|
62
|
-
|
|
182
|
+
/** @type {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} */
|
|
183
|
+
let taskTimingsByEvent;
|
|
63
184
|
|
|
64
185
|
if (settings.throttlingMethod === 'simulate') {
|
|
186
|
+
taskTimingsByEvent = new Map();
|
|
187
|
+
|
|
65
188
|
const simulatorOptions = {devtoolsLog, settings: context.settings};
|
|
66
189
|
const pageGraph = await PageDependencyGraph.request({trace, devtoolsLog, URL}, context);
|
|
67
190
|
const simulator = await LoadSimulator.request(simulatorOptions, context);
|
|
@@ -70,30 +193,32 @@ class LongTasks extends Audit {
|
|
|
70
193
|
if (node.type !== 'cpu') continue;
|
|
71
194
|
taskTimingsByEvent.set(node.event, timing);
|
|
72
195
|
}
|
|
73
|
-
} else {
|
|
74
|
-
for (const task of tasks) {
|
|
75
|
-
if (task.unbounded || task.parent) continue;
|
|
76
|
-
taskTimingsByEvent.set(task.event, task);
|
|
77
|
-
}
|
|
78
196
|
}
|
|
79
197
|
|
|
80
198
|
const jsURLs = getJavaScriptURLs(networkRecords);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
199
|
+
|
|
200
|
+
// Only consider top-level (no parent) long tasks that have an explicit endTime.
|
|
201
|
+
const longTasks = tasks
|
|
202
|
+
.map(task => {
|
|
203
|
+
// Use duration from simulation, if available.
|
|
204
|
+
const {duration} = LongTasks.getTiming(task, taskTimingsByEvent);
|
|
205
|
+
return {task, duration};
|
|
206
|
+
})
|
|
207
|
+
.filter(({task, duration}) => {
|
|
208
|
+
return duration >= 50 && !task.unbounded && !task.parent;
|
|
86
209
|
})
|
|
87
|
-
.filter(t => t.duration >= 50 && !t.unbounded && !t.parent)
|
|
88
210
|
.sort((a, b) => b.duration - a.duration)
|
|
89
|
-
.
|
|
211
|
+
.map(({task}) => task);
|
|
90
212
|
|
|
91
213
|
// TODO(beytoven): Add start time that matches with the simulated throttling
|
|
92
|
-
const results =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
214
|
+
const results = longTasks.map(task => {
|
|
215
|
+
const timing = LongTasks.getTiming(task, taskTimingsByEvent);
|
|
216
|
+
return {
|
|
217
|
+
url: getAttributableURLForTask(task, jsURLs),
|
|
218
|
+
duration: timing.duration,
|
|
219
|
+
startTime: timing.startTime,
|
|
220
|
+
};
|
|
221
|
+
}).slice(0, DISPLAYED_TASK_COUNT);
|
|
97
222
|
|
|
98
223
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
99
224
|
const headings = [
|
|
@@ -107,6 +232,8 @@ class LongTasks extends Audit {
|
|
|
107
232
|
const tableDetails = Audit.makeTableDetails(headings, results,
|
|
108
233
|
{sortedBy: ['duration'], skipSumming: ['startTime']});
|
|
109
234
|
|
|
235
|
+
tableDetails.debugData = LongTasks.makeDebugData(longTasks, jsURLs, taskTimingsByEvent);
|
|
236
|
+
|
|
110
237
|
let displayValue;
|
|
111
238
|
if (results.length > 0) {
|
|
112
239
|
displayValue = str_(UIStrings.displayValue, {itemCount: results.length});
|
|
@@ -125,6 +125,7 @@ class NonCompositedAnimations extends Audit {
|
|
|
125
125
|
return {
|
|
126
126
|
score: 1,
|
|
127
127
|
notApplicable: true,
|
|
128
|
+
metricSavings: {CLS: 0},
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -195,6 +196,12 @@ class NonCompositedAnimations extends Audit {
|
|
|
195
196
|
return {
|
|
196
197
|
score: results.length === 0 ? 1 : 0,
|
|
197
198
|
notApplicable: results.length === 0,
|
|
199
|
+
metricSavings: {
|
|
200
|
+
// We do not have enough information to accurately predict the impact of individual animations on CLS.
|
|
201
|
+
// It is also not worth the effort since only a small percentage of sites have their CLS affected by non-composited animations.
|
|
202
|
+
// https://github.com/GoogleChrome/lighthouse/pull/15099#issuecomment-1558107906
|
|
203
|
+
CLS: 0,
|
|
204
|
+
},
|
|
198
205
|
details,
|
|
199
206
|
displayValue,
|
|
200
207
|
};
|
|
@@ -244,7 +244,7 @@ class PrioritizeLcpImage extends Audit {
|
|
|
244
244
|
.find(element => element.traceEventType === 'largest-contentful-paint');
|
|
245
245
|
|
|
246
246
|
if (!lcpElement || lcpElement.type !== 'image') {
|
|
247
|
-
return {score: null, notApplicable: true};
|
|
247
|
+
return {score: null, notApplicable: true, metricSavings: {LCP: 0}};
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
const mainResource = await MainResource.request({devtoolsLog, URL}, context);
|
|
@@ -286,6 +286,7 @@ class PrioritizeLcpImage extends Audit {
|
|
|
286
286
|
numericUnit: 'millisecond',
|
|
287
287
|
displayValue: wastedMs ? str_(i18n.UIStrings.displayValueMsSavings, {wastedMs}) : '',
|
|
288
288
|
details,
|
|
289
|
+
metricSavings: {LCP: wastedMs},
|
|
289
290
|
};
|
|
290
291
|
}
|
|
291
292
|
}
|
package/core/audits/redirects.js
CHANGED
|
@@ -74,6 +74,16 @@ const BLOCKLIST = new Set([
|
|
|
74
74
|
'mer',
|
|
75
75
|
'mer info',
|
|
76
76
|
'mer information',
|
|
77
|
+
// Tamil
|
|
78
|
+
'அடுத்த பக்கம்',
|
|
79
|
+
'மறுபக்கம்',
|
|
80
|
+
'முந்தைய பக்கம்',
|
|
81
|
+
'முன்பக்கம்',
|
|
82
|
+
'மேலும் அறிக',
|
|
83
|
+
'மேலும் தகவலுக்கு',
|
|
84
|
+
'மேலும் தரவுகளுக்கு',
|
|
85
|
+
'தயவுசெய்து இங்கே அழுத்தவும்',
|
|
86
|
+
'இங்கே கிளிக் செய்யவும்',
|
|
77
87
|
]);
|
|
78
88
|
|
|
79
89
|
const UIStrings = {
|
package/core/audits/viewport.js
CHANGED
|
@@ -46,15 +46,26 @@ class Viewport extends Audit {
|
|
|
46
46
|
static async audit(artifacts, context) {
|
|
47
47
|
const viewportMeta = await ViewportMeta.request(artifacts.MetaElements, context);
|
|
48
48
|
|
|
49
|
+
let inpSavings = 300;
|
|
49
50
|
if (!viewportMeta.hasViewportTag) {
|
|
50
51
|
return {
|
|
51
52
|
score: 0,
|
|
52
53
|
explanation: str_(UIStrings.explanationNoTag),
|
|
54
|
+
metricSavings: {
|
|
55
|
+
INP: inpSavings,
|
|
56
|
+
},
|
|
53
57
|
};
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
if (viewportMeta.isMobileOptimized) {
|
|
61
|
+
inpSavings = 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
return {
|
|
57
65
|
score: Number(viewportMeta.isMobileOptimized),
|
|
66
|
+
metricSavings: {
|
|
67
|
+
INP: inpSavings,
|
|
68
|
+
},
|
|
58
69
|
warnings: viewportMeta.parserWarnings,
|
|
59
70
|
};
|
|
60
71
|
}
|
|
@@ -224,7 +224,11 @@ class WorkDuringInteraction extends Audit {
|
|
|
224
224
|
const {settings} = context;
|
|
225
225
|
// TODO: responsiveness isn't yet supported by lantern.
|
|
226
226
|
if (settings.throttlingMethod === 'simulate') {
|
|
227
|
-
return {
|
|
227
|
+
return {
|
|
228
|
+
score: null,
|
|
229
|
+
notApplicable: true,
|
|
230
|
+
metricSavings: {INP: 0},
|
|
231
|
+
};
|
|
228
232
|
}
|
|
229
233
|
|
|
230
234
|
const trace = artifacts.traces[WorkDuringInteraction.DEFAULT_PASS];
|
|
@@ -232,7 +236,11 @@ class WorkDuringInteraction extends Audit {
|
|
|
232
236
|
const interactionEvent = await Responsiveness.request(metricData, context);
|
|
233
237
|
// If no interaction, diagnostic audit is n/a.
|
|
234
238
|
if (interactionEvent === null) {
|
|
235
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
score: null,
|
|
241
|
+
notApplicable: true,
|
|
242
|
+
metricSavings: {INP: 0},
|
|
243
|
+
};
|
|
236
244
|
}
|
|
237
245
|
// TODO: remove workaround once 103.0.5052.0 is sufficiently released.
|
|
238
246
|
if (interactionEvent.name === 'FallbackTiming') {
|
|
@@ -271,6 +279,9 @@ class WorkDuringInteraction extends Audit {
|
|
|
271
279
|
type: 'list',
|
|
272
280
|
items: auditDetailsItems,
|
|
273
281
|
},
|
|
282
|
+
metricSavings: {
|
|
283
|
+
INP: duration,
|
|
284
|
+
},
|
|
274
285
|
};
|
|
275
286
|
}
|
|
276
287
|
}
|
|
@@ -99,7 +99,7 @@ class JSBundles {
|
|
|
99
99
|
|
|
100
100
|
const compiledUrl = SourceMap.scriptUrl || 'compiled.js';
|
|
101
101
|
const mapUrl = SourceMap.sourceMapUrl || 'compiled.js.map';
|
|
102
|
-
const map = new SDK.
|
|
102
|
+
const map = new SDK.SourceMap(compiledUrl, mapUrl, rawMap);
|
|
103
103
|
|
|
104
104
|
const sizes = computeGeneratedFileSizes(map, script.length || 0, script.content || '');
|
|
105
105
|
|
|
@@ -15,4 +15,30 @@ export function calculateSumOfBlockingTime(topLevelEvents: Array<{
|
|
|
15
15
|
end: number;
|
|
16
16
|
duration: number;
|
|
17
17
|
}>, startTimeMs: number, endTimeMs: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* For TBT, We only want to consider tasks that fall in our time range
|
|
20
|
+
* - FCP and TTI for navigation mode
|
|
21
|
+
* - Trace start and trace end for timespan mode
|
|
22
|
+
*
|
|
23
|
+
* FCP is picked as `startTimeMs` because there is little risk of user input happening
|
|
24
|
+
* before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
|
|
25
|
+
* optimizing to reach FCP as fast as possible without having to worry about task lengths.
|
|
26
|
+
*
|
|
27
|
+
* TTI is picked as `endTimeMs` because we want a well defined end point for page load.
|
|
28
|
+
*
|
|
29
|
+
* @param {{start: number, end: number, duration: number}} event
|
|
30
|
+
* @param {number} startTimeMs Should be FCP in navigation mode and the trace start time in timespan mode
|
|
31
|
+
* @param {number} endTimeMs Should be TTI in navigation mode and the trace end time in timespan mode
|
|
32
|
+
* @param {{start: number, end: number, duration: number}} [topLevelEvent] Leave unset if `event` is top level. Has no effect if `event` has the same duration as `topLevelEvent`.
|
|
33
|
+
* @return {number}
|
|
34
|
+
*/
|
|
35
|
+
export function calculateTbtImpactForEvent(event: {
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
duration: number;
|
|
39
|
+
}, startTimeMs: number, endTimeMs: number, topLevelEvent?: {
|
|
40
|
+
start: number;
|
|
41
|
+
end: number;
|
|
42
|
+
duration: number;
|
|
43
|
+
} | undefined): number;
|
|
18
44
|
//# sourceMappingURL=tbt-utils.d.ts.map
|