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
|
@@ -6,6 +6,52 @@
|
|
|
6
6
|
|
|
7
7
|
const BLOCKING_TIME_THRESHOLD = 50;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* For TBT, We only want to consider tasks that fall in our time range
|
|
11
|
+
* - FCP and TTI for navigation mode
|
|
12
|
+
* - Trace start and trace end for timespan mode
|
|
13
|
+
*
|
|
14
|
+
* FCP is picked as `startTimeMs` because there is little risk of user input happening
|
|
15
|
+
* before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
|
|
16
|
+
* optimizing to reach FCP as fast as possible without having to worry about task lengths.
|
|
17
|
+
*
|
|
18
|
+
* TTI is picked as `endTimeMs` because we want a well defined end point for page load.
|
|
19
|
+
*
|
|
20
|
+
* @param {{start: number, end: number, duration: number}} event
|
|
21
|
+
* @param {number} startTimeMs Should be FCP in navigation mode and the trace start time in timespan mode
|
|
22
|
+
* @param {number} endTimeMs Should be TTI in navigation mode and the trace end time in timespan mode
|
|
23
|
+
* @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`.
|
|
24
|
+
* @return {number}
|
|
25
|
+
*/
|
|
26
|
+
function calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent) {
|
|
27
|
+
let threshold = BLOCKING_TIME_THRESHOLD;
|
|
28
|
+
|
|
29
|
+
// If a task is not top level, it doesn't make sense to subtract the entire 50ms
|
|
30
|
+
// blocking threshold from the event.
|
|
31
|
+
//
|
|
32
|
+
// e.g. A 80ms top level task with two 40ms children should attribute some blocking
|
|
33
|
+
// time to the 40ms tasks even though they do not meet the 50ms threshold.
|
|
34
|
+
//
|
|
35
|
+
// The solution is to scale the threshold for child events to be considered blocking.
|
|
36
|
+
if (topLevelEvent) threshold *= (event.duration / topLevelEvent.duration);
|
|
37
|
+
|
|
38
|
+
if (event.duration < threshold) return 0;
|
|
39
|
+
if (event.end < startTimeMs) return 0;
|
|
40
|
+
if (event.start > endTimeMs) return 0;
|
|
41
|
+
|
|
42
|
+
// Perform the clipping and then calculate Blocking Region. So if we have a 150ms task
|
|
43
|
+
// [0, 150] and `startTimeMs` is at 50ms, we first clip the task to [50, 150], and then
|
|
44
|
+
// calculate the Blocking Region to be [100, 150]. The rational here is that tasks before
|
|
45
|
+
// the start time are unimportant, so we care whether the main thread is busy more than
|
|
46
|
+
// 50ms at a time only after the start time.
|
|
47
|
+
const clippedStart = Math.max(event.start, startTimeMs);
|
|
48
|
+
const clippedEnd = Math.min(event.end, endTimeMs);
|
|
49
|
+
const clippedDuration = clippedEnd - clippedStart;
|
|
50
|
+
if (clippedDuration < threshold) return 0;
|
|
51
|
+
|
|
52
|
+
return clippedDuration - threshold;
|
|
53
|
+
}
|
|
54
|
+
|
|
9
55
|
/**
|
|
10
56
|
* @param {Array<{start: number, end: number, duration: number}>} topLevelEvents
|
|
11
57
|
* @param {number} startTimeMs
|
|
@@ -15,36 +61,9 @@ const BLOCKING_TIME_THRESHOLD = 50;
|
|
|
15
61
|
function calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
|
|
16
62
|
if (endTimeMs <= startTimeMs) return 0;
|
|
17
63
|
|
|
18
|
-
const threshold = BLOCKING_TIME_THRESHOLD;
|
|
19
64
|
let sumBlockingTime = 0;
|
|
20
65
|
for (const event of topLevelEvents) {
|
|
21
|
-
|
|
22
|
-
if (event.duration < threshold) continue;
|
|
23
|
-
|
|
24
|
-
// We only want to consider tasks that fall in our time range (FCP and TTI for navigations).
|
|
25
|
-
// FCP is picked as the lower bound because there is little risk of user input happening
|
|
26
|
-
// before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
|
|
27
|
-
// optimizing to reach FCP as fast as possible without having to worry about task lengths.
|
|
28
|
-
if (event.end < startTimeMs) continue;
|
|
29
|
-
|
|
30
|
-
// TTI is picked as the upper bound because we want a well defined end point for page load.
|
|
31
|
-
if (event.start > endTimeMs) continue;
|
|
32
|
-
|
|
33
|
-
// We first perform the clipping, and then calculate Blocking Region. So if we have a 150ms
|
|
34
|
-
// task [0, 150] and FCP happens midway at 50ms, we first clip the task to [50, 150], and then
|
|
35
|
-
// calculate the Blocking Region to be [100, 150]. The rational here is that tasks before FCP
|
|
36
|
-
// are unimportant, so we care whether the main thread is busy more than 50ms at a time only
|
|
37
|
-
// after FCP.
|
|
38
|
-
const clippedStart = Math.max(event.start, startTimeMs);
|
|
39
|
-
const clippedEnd = Math.min(event.end, endTimeMs);
|
|
40
|
-
const clippedDuration = clippedEnd - clippedStart;
|
|
41
|
-
if (clippedDuration < threshold) continue;
|
|
42
|
-
|
|
43
|
-
// The duration of the task beyond 50ms at the beginning is considered the Blocking Region.
|
|
44
|
-
// Example:
|
|
45
|
-
// [ 250ms Task ]
|
|
46
|
-
// | First 50ms | Blocking Region (200ms) |
|
|
47
|
-
sumBlockingTime += clippedDuration - threshold;
|
|
66
|
+
sumBlockingTime += calculateTbtImpactForEvent(event, startTimeMs, endTimeMs);
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
return sumBlockingTime;
|
|
@@ -53,4 +72,5 @@ function calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
|
|
|
53
72
|
export {
|
|
54
73
|
BLOCKING_TIME_THRESHOLD,
|
|
55
74
|
calculateSumOfBlockingTime,
|
|
75
|
+
calculateTbtImpactForEvent,
|
|
56
76
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export { TBTImpactTasksComputed as TBTImpactTasks };
|
|
2
|
+
export type TBTImpactTask = LH.Artifacts.TaskNode & {
|
|
3
|
+
tbtImpact: number;
|
|
4
|
+
selfTbtImpact: number;
|
|
5
|
+
};
|
|
6
|
+
declare const TBTImpactTasksComputed: typeof TBTImpactTasks & {
|
|
7
|
+
request: (dependencies: import("../index.js").Artifacts.MetricComputationDataInput, context: import("../../types/utility-types.js").default.ImmutableObject<{
|
|
8
|
+
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
|
|
9
|
+
}>) => Promise<TBTImpactTask[]>;
|
|
10
|
+
};
|
|
11
|
+
/** @typedef {LH.Artifacts.TaskNode & {tbtImpact: number, selfTbtImpact: number}} TBTImpactTask */
|
|
12
|
+
declare class TBTImpactTasks {
|
|
13
|
+
/**
|
|
14
|
+
* @param {LH.Artifacts.TaskNode} task
|
|
15
|
+
* @return {LH.Artifacts.TaskNode}
|
|
16
|
+
*/
|
|
17
|
+
static getTopLevelTask(task: LH.Artifacts.TaskNode): LH.Artifacts.TaskNode;
|
|
18
|
+
/**
|
|
19
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
20
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
21
|
+
* @return {Promise<{startTimeMs: number, endTimeMs: number}>}
|
|
22
|
+
*/
|
|
23
|
+
static getTbtBounds(metricComputationData: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
|
|
24
|
+
startTimeMs: number;
|
|
25
|
+
endTimeMs: number;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
29
|
+
* @param {Map<LH.Artifacts.TaskNode, number>} taskToImpact
|
|
30
|
+
*/
|
|
31
|
+
static createImpactTasks(tasks: LH.Artifacts.TaskNode[], taskToImpact: Map<LH.Artifacts.TaskNode, number>): TBTImpactTask[];
|
|
32
|
+
/**
|
|
33
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
34
|
+
* @param {number} startTimeMs
|
|
35
|
+
* @param {number} endTimeMs
|
|
36
|
+
* @return {TBTImpactTask[]}
|
|
37
|
+
*/
|
|
38
|
+
static computeImpactsFromObservedTasks(tasks: LH.Artifacts.TaskNode[], startTimeMs: number, endTimeMs: number): TBTImpactTask[];
|
|
39
|
+
/**
|
|
40
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
41
|
+
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} tbtNodeTimings
|
|
42
|
+
* @param {number} startTimeMs
|
|
43
|
+
* @param {number} endTimeMs
|
|
44
|
+
* @return {TBTImpactTask[]}
|
|
45
|
+
*/
|
|
46
|
+
static computeImpactsFromLantern(tasks: LH.Artifacts.TaskNode[], tbtNodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], startTimeMs: number, endTimeMs: number): TBTImpactTask[];
|
|
47
|
+
/**
|
|
48
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
49
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
50
|
+
* @return {Promise<TBTImpactTask[]>}
|
|
51
|
+
*/
|
|
52
|
+
static compute_(metricComputationData: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<TBTImpactTask[]>;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=tbt-impact-tasks.d.ts.map
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright 2023 The Lighthouse Authors. All Rights Reserved.
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
4
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {makeComputedArtifact} from './computed-artifact.js';
|
|
8
|
+
import {MainThreadTasks} from './main-thread-tasks.js';
|
|
9
|
+
import {FirstContentfulPaint} from './metrics/first-contentful-paint.js';
|
|
10
|
+
import {Interactive} from './metrics/interactive.js';
|
|
11
|
+
import {TotalBlockingTime} from './metrics/total-blocking-time.js';
|
|
12
|
+
import {ProcessedTrace} from './processed-trace.js';
|
|
13
|
+
import {calculateTbtImpactForEvent} from './metrics/tbt-utils.js';
|
|
14
|
+
|
|
15
|
+
/** @typedef {LH.Artifacts.TaskNode & {tbtImpact: number, selfTbtImpact: number}} TBTImpactTask */
|
|
16
|
+
|
|
17
|
+
class TBTImpactTasks {
|
|
18
|
+
/**
|
|
19
|
+
* @param {LH.Artifacts.TaskNode} task
|
|
20
|
+
* @return {LH.Artifacts.TaskNode}
|
|
21
|
+
*/
|
|
22
|
+
static getTopLevelTask(task) {
|
|
23
|
+
let topLevelTask = task;
|
|
24
|
+
while (topLevelTask.parent) {
|
|
25
|
+
topLevelTask = topLevelTask.parent;
|
|
26
|
+
}
|
|
27
|
+
return topLevelTask;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
32
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
33
|
+
* @return {Promise<{startTimeMs: number, endTimeMs: number}>}
|
|
34
|
+
*/
|
|
35
|
+
static async getTbtBounds(metricComputationData, context) {
|
|
36
|
+
const processedTrace = await ProcessedTrace.request(metricComputationData.trace, context);
|
|
37
|
+
if (metricComputationData.gatherContext.gatherMode !== 'navigation') {
|
|
38
|
+
return {
|
|
39
|
+
startTimeMs: 0,
|
|
40
|
+
endTimeMs: processedTrace.timings.traceEnd,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fcpResult = await FirstContentfulPaint.request(metricComputationData, context);
|
|
45
|
+
const ttiResult = await Interactive.request(metricComputationData, context);
|
|
46
|
+
|
|
47
|
+
let startTimeMs = fcpResult.timing;
|
|
48
|
+
let endTimeMs = ttiResult.timing;
|
|
49
|
+
|
|
50
|
+
// When using lantern, we want to get a pessimistic view of the long tasks.
|
|
51
|
+
// This means we assume the earliest possible start time and latest possible end time.
|
|
52
|
+
|
|
53
|
+
if ('optimisticEstimate' in fcpResult) {
|
|
54
|
+
startTimeMs = fcpResult.optimisticEstimate.timeInMs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ('pessimisticEstimate' in ttiResult) {
|
|
58
|
+
endTimeMs = ttiResult.pessimisticEstimate.timeInMs;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {startTimeMs, endTimeMs};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
66
|
+
* @param {Map<LH.Artifacts.TaskNode, number>} taskToImpact
|
|
67
|
+
*/
|
|
68
|
+
static createImpactTasks(tasks, taskToImpact) {
|
|
69
|
+
/** @type {TBTImpactTask[]} */
|
|
70
|
+
const tbtImpactTasks = [];
|
|
71
|
+
|
|
72
|
+
for (const task of tasks) {
|
|
73
|
+
const tbtImpact = taskToImpact.get(task) || 0;
|
|
74
|
+
let selfTbtImpact = tbtImpact;
|
|
75
|
+
|
|
76
|
+
for (const child of task.children) {
|
|
77
|
+
const childTbtImpact = taskToImpact.get(child) || 0;
|
|
78
|
+
selfTbtImpact -= childTbtImpact;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
tbtImpactTasks.push({
|
|
82
|
+
...task,
|
|
83
|
+
tbtImpact,
|
|
84
|
+
selfTbtImpact,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return tbtImpactTasks;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
93
|
+
* @param {number} startTimeMs
|
|
94
|
+
* @param {number} endTimeMs
|
|
95
|
+
* @return {TBTImpactTask[]}
|
|
96
|
+
*/
|
|
97
|
+
static computeImpactsFromObservedTasks(tasks, startTimeMs, endTimeMs) {
|
|
98
|
+
/** @type {Map<LH.Artifacts.TaskNode, number>} */
|
|
99
|
+
const taskToImpact = new Map();
|
|
100
|
+
|
|
101
|
+
for (const task of tasks) {
|
|
102
|
+
const event = {
|
|
103
|
+
start: task.startTime,
|
|
104
|
+
end: task.endTime,
|
|
105
|
+
duration: task.duration,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const topLevelTask = this.getTopLevelTask(task);
|
|
109
|
+
const topLevelEvent = {
|
|
110
|
+
start: topLevelTask.startTime,
|
|
111
|
+
end: topLevelTask.endTime,
|
|
112
|
+
duration: topLevelTask.duration,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent);
|
|
116
|
+
|
|
117
|
+
taskToImpact.set(task, tbtImpact);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return this.createImpactTasks(tasks, taskToImpact);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
125
|
+
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} tbtNodeTimings
|
|
126
|
+
* @param {number} startTimeMs
|
|
127
|
+
* @param {number} endTimeMs
|
|
128
|
+
* @return {TBTImpactTask[]}
|
|
129
|
+
*/
|
|
130
|
+
static computeImpactsFromLantern(tasks, tbtNodeTimings, startTimeMs, endTimeMs) {
|
|
131
|
+
/** @type {Map<LH.Artifacts.TaskNode, number>} */
|
|
132
|
+
const taskToImpact = new Map();
|
|
133
|
+
|
|
134
|
+
/** @type {Map<LH.Artifacts.TaskNode, {start: number, end: number, duration: number}>} */
|
|
135
|
+
const topLevelTaskToEvent = new Map();
|
|
136
|
+
|
|
137
|
+
/** @type {Map<LH.TraceEvent, LH.Artifacts.TaskNode>} */
|
|
138
|
+
const traceEventToTask = new Map();
|
|
139
|
+
for (const task of tasks) {
|
|
140
|
+
traceEventToTask.set(task.event, task);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Use lantern TBT timings to calculate the TBT impact of top level tasks.
|
|
144
|
+
for (const [node, timing] of tbtNodeTimings) {
|
|
145
|
+
if (node.type !== 'cpu') continue;
|
|
146
|
+
|
|
147
|
+
const event = {
|
|
148
|
+
start: timing.startTime,
|
|
149
|
+
end: timing.endTime,
|
|
150
|
+
duration: timing.duration,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs);
|
|
154
|
+
|
|
155
|
+
const task = traceEventToTask.get(node.event);
|
|
156
|
+
if (!task) continue;
|
|
157
|
+
|
|
158
|
+
topLevelTaskToEvent.set(task, event);
|
|
159
|
+
taskToImpact.set(task, tbtImpact);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Interpolate the TBT impact of remaining tasks using the top level ancestor tasks.
|
|
163
|
+
// We don't have any lantern estimates for tasks that are not top level, so we need to estimate
|
|
164
|
+
// the lantern timing based on the task's observed timing relative to it's top level task's observed timing.
|
|
165
|
+
for (const task of tasks) {
|
|
166
|
+
if (taskToImpact.has(task)) continue;
|
|
167
|
+
|
|
168
|
+
const topLevelTask = this.getTopLevelTask(task);
|
|
169
|
+
|
|
170
|
+
const topLevelEvent = topLevelTaskToEvent.get(topLevelTask);
|
|
171
|
+
if (!topLevelEvent) continue;
|
|
172
|
+
|
|
173
|
+
const startRatio = (task.startTime - topLevelTask.startTime) / topLevelTask.duration;
|
|
174
|
+
const start = startRatio * topLevelEvent.duration + topLevelEvent.start;
|
|
175
|
+
|
|
176
|
+
const endRatio = (topLevelTask.endTime - task.endTime) / topLevelTask.duration;
|
|
177
|
+
const end = topLevelEvent.end - endRatio * topLevelEvent.duration;
|
|
178
|
+
|
|
179
|
+
const event = {
|
|
180
|
+
start,
|
|
181
|
+
end,
|
|
182
|
+
duration: end - start,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent);
|
|
186
|
+
|
|
187
|
+
taskToImpact.set(task, tbtImpact);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return this.createImpactTasks(tasks, taskToImpact);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
195
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
196
|
+
* @return {Promise<TBTImpactTask[]>}
|
|
197
|
+
*/
|
|
198
|
+
static async compute_(metricComputationData, context) {
|
|
199
|
+
const tbtResult = await TotalBlockingTime.request(metricComputationData, context);
|
|
200
|
+
const tasks = await MainThreadTasks.request(metricComputationData.trace, context);
|
|
201
|
+
|
|
202
|
+
const {startTimeMs, endTimeMs} = await this.getTbtBounds(metricComputationData, context);
|
|
203
|
+
|
|
204
|
+
if ('pessimisticEstimate' in tbtResult) {
|
|
205
|
+
return this.computeImpactsFromLantern(
|
|
206
|
+
tasks,
|
|
207
|
+
tbtResult.pessimisticEstimate.nodeTimings,
|
|
208
|
+
startTimeMs,
|
|
209
|
+
endTimeMs
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return this.computeImpactsFromObservedTasks(tasks, startTimeMs, endTimeMs);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const TBTImpactTasksComputed = makeComputedArtifact(
|
|
218
|
+
TBTImpactTasks,
|
|
219
|
+
['trace', 'devtoolsLog', 'URL', 'gatherContext', 'settings', 'simulator']
|
|
220
|
+
);
|
|
221
|
+
export {TBTImpactTasksComputed as TBTImpactTasks};
|
|
@@ -236,6 +236,7 @@ const defaultConfig = {
|
|
|
236
236
|
'accessibility/accesskeys',
|
|
237
237
|
'accessibility/aria-allowed-attr',
|
|
238
238
|
'accessibility/aria-command-name',
|
|
239
|
+
'accessibility/aria-dialog-name',
|
|
239
240
|
'accessibility/aria-hidden-body',
|
|
240
241
|
'accessibility/aria-hidden-focus',
|
|
241
242
|
'accessibility/aria-input-field-name',
|
|
@@ -245,6 +246,7 @@ const defaultConfig = {
|
|
|
245
246
|
'accessibility/aria-required-children',
|
|
246
247
|
'accessibility/aria-required-parent',
|
|
247
248
|
'accessibility/aria-roles',
|
|
249
|
+
'accessibility/aria-text',
|
|
248
250
|
'accessibility/aria-toggle-field-name',
|
|
249
251
|
'accessibility/aria-tooltip-name',
|
|
250
252
|
'accessibility/aria-treeitem-name',
|
|
@@ -258,24 +260,30 @@ const defaultConfig = {
|
|
|
258
260
|
'accessibility/document-title',
|
|
259
261
|
'accessibility/duplicate-id-active',
|
|
260
262
|
'accessibility/duplicate-id-aria',
|
|
263
|
+
'accessibility/empty-heading',
|
|
261
264
|
'accessibility/form-field-multiple-labels',
|
|
262
265
|
'accessibility/frame-title',
|
|
263
266
|
'accessibility/heading-order',
|
|
264
267
|
'accessibility/html-has-lang',
|
|
265
268
|
'accessibility/html-lang-valid',
|
|
266
269
|
'accessibility/html-xml-lang-mismatch',
|
|
270
|
+
'accessibility/identical-links-same-purpose',
|
|
267
271
|
'accessibility/image-alt',
|
|
268
272
|
'accessibility/input-button-name',
|
|
269
273
|
'accessibility/input-image-alt',
|
|
270
274
|
'accessibility/label',
|
|
275
|
+
'accessibility/landmark-one-main',
|
|
271
276
|
'accessibility/link-name',
|
|
277
|
+
'accessibility/link-in-text-block',
|
|
272
278
|
'accessibility/list',
|
|
273
279
|
'accessibility/listitem',
|
|
274
280
|
'accessibility/meta-refresh',
|
|
275
281
|
'accessibility/meta-viewport',
|
|
276
282
|
'accessibility/object-alt',
|
|
283
|
+
'accessibility/select-name',
|
|
277
284
|
'accessibility/tabindex',
|
|
278
285
|
'accessibility/table-fake-caption',
|
|
286
|
+
'accessibility/target-size',
|
|
279
287
|
'accessibility/td-has-header',
|
|
280
288
|
'accessibility/td-headers-attr',
|
|
281
289
|
'accessibility/th-has-data-cells',
|
|
@@ -504,6 +512,7 @@ const defaultConfig = {
|
|
|
504
512
|
{id: 'accesskeys', weight: 3, group: 'a11y-navigation'},
|
|
505
513
|
{id: 'aria-allowed-attr', weight: 10, group: 'a11y-aria'},
|
|
506
514
|
{id: 'aria-command-name', weight: 3, group: 'a11y-aria'},
|
|
515
|
+
{id: 'aria-dialog-name', weight: 3, group: 'a11y-aria'},
|
|
507
516
|
{id: 'aria-hidden-body', weight: 10, group: 'a11y-aria'},
|
|
508
517
|
{id: 'aria-hidden-focus', weight: 3, group: 'a11y-aria'},
|
|
509
518
|
{id: 'aria-input-field-name', weight: 3, group: 'a11y-aria'},
|
|
@@ -513,6 +522,7 @@ const defaultConfig = {
|
|
|
513
522
|
{id: 'aria-required-children', weight: 10, group: 'a11y-aria'},
|
|
514
523
|
{id: 'aria-required-parent', weight: 10, group: 'a11y-aria'},
|
|
515
524
|
{id: 'aria-roles', weight: 10, group: 'a11y-aria'},
|
|
525
|
+
{id: 'aria-text', weight: 3, group: 'a11y-aria'},
|
|
516
526
|
{id: 'aria-toggle-field-name', weight: 3, group: 'a11y-aria'},
|
|
517
527
|
{id: 'aria-tooltip-name', weight: 3, group: 'a11y-aria'},
|
|
518
528
|
{id: 'aria-treeitem-name', weight: 3, group: 'a11y-aria'},
|
|
@@ -536,12 +546,14 @@ const defaultConfig = {
|
|
|
536
546
|
{id: 'input-button-name', weight: 10, group: 'a11y-names-labels'},
|
|
537
547
|
{id: 'input-image-alt', weight: 10, group: 'a11y-names-labels'},
|
|
538
548
|
{id: 'label', weight: 10, group: 'a11y-names-labels'},
|
|
549
|
+
{id: 'link-in-text-block', weight: 3, group: 'a11y-color-contrast'},
|
|
539
550
|
{id: 'link-name', weight: 3, group: 'a11y-names-labels'},
|
|
540
551
|
{id: 'list', weight: 3, group: 'a11y-tables-lists'},
|
|
541
552
|
{id: 'listitem', weight: 3, group: 'a11y-tables-lists'},
|
|
542
553
|
{id: 'meta-refresh', weight: 10, group: 'a11y-best-practices'},
|
|
543
554
|
{id: 'meta-viewport', weight: 10, group: 'a11y-best-practices'},
|
|
544
555
|
{id: 'object-alt', weight: 3, group: 'a11y-names-labels'},
|
|
556
|
+
{id: 'select-name', weight: 3, group: 'a11y-names-labels'},
|
|
545
557
|
{id: 'tabindex', weight: 3, group: 'a11y-navigation'},
|
|
546
558
|
{id: 'table-fake-caption', weight: 3, group: 'a11y-tables-lists'},
|
|
547
559
|
{id: 'td-has-header', weight: 10, group: 'a11y-tables-lists'},
|
|
@@ -560,6 +572,11 @@ const defaultConfig = {
|
|
|
560
572
|
{id: 'visual-order-follows-dom', weight: 0},
|
|
561
573
|
{id: 'offscreen-content-hidden', weight: 0},
|
|
562
574
|
{id: 'use-landmarks', weight: 0},
|
|
575
|
+
// Hidden audits
|
|
576
|
+
{id: 'empty-heading', weight: 0, group: 'hidden'},
|
|
577
|
+
{id: 'identical-links-same-purpose', weight: 0, group: 'hidden'},
|
|
578
|
+
{id: 'landmark-one-main', weight: 0, group: 'hidden'},
|
|
579
|
+
{id: 'target-size', weight: 0, group: 'hidden'},
|
|
563
580
|
],
|
|
564
581
|
},
|
|
565
582
|
'best-practices': {
|
package/core/config/filters.d.ts
CHANGED
|
@@ -59,25 +59,25 @@ export function filterAuditsByGatherMode(audits: LH.Config.ResolvedConfig['audit
|
|
|
59
59
|
* Filters a categories object and their auditRefs down to the set that can be computed using
|
|
60
60
|
* only the specified audits.
|
|
61
61
|
*
|
|
62
|
-
* @param {LH.Config.
|
|
62
|
+
* @param {LH.Config.ResolvedConfig['categories']} categories
|
|
63
63
|
* @param {Array<LH.Config.AuditDefn>} availableAudits
|
|
64
|
-
* @return {LH.Config.
|
|
64
|
+
* @return {LH.Config.ResolvedConfig['categories']}
|
|
65
65
|
*/
|
|
66
|
-
export function filterCategoriesByAvailableAudits(categories: LH.Config.
|
|
66
|
+
export function filterCategoriesByAvailableAudits(categories: LH.Config.ResolvedConfig['categories'], availableAudits: Array<LH.Config.AuditDefn>): LH.Config.ResolvedConfig['categories'];
|
|
67
67
|
/**
|
|
68
68
|
* Filters a categories object and their auditRefs down to the specified category ids.
|
|
69
69
|
*
|
|
70
|
-
* @param {LH.Config.
|
|
70
|
+
* @param {LH.Config.ResolvedConfig['categories']} categories
|
|
71
71
|
* @param {string[] | null | undefined} onlyCategories
|
|
72
|
-
* @return {LH.Config.
|
|
72
|
+
* @return {LH.Config.ResolvedConfig['categories']}
|
|
73
73
|
*/
|
|
74
|
-
export function filterCategoriesByExplicitFilters(categories: LH.Config.
|
|
74
|
+
export function filterCategoriesByExplicitFilters(categories: LH.Config.ResolvedConfig['categories'], onlyCategories: string[] | null | undefined): LH.Config.ResolvedConfig['categories'];
|
|
75
75
|
/**
|
|
76
76
|
* Optional `supportedModes` property can explicitly exclude a category even if some audits are available.
|
|
77
77
|
*
|
|
78
|
-
* @param {LH.Config.
|
|
78
|
+
* @param {LH.Config.ResolvedConfig['categories']} categories
|
|
79
79
|
* @param {LH.Gatherer.GatherMode} mode
|
|
80
|
-
* @return {LH.Config.
|
|
80
|
+
* @return {LH.Config.ResolvedConfig['categories']}
|
|
81
81
|
*/
|
|
82
|
-
export function filterCategoriesByGatherMode(categories: LH.Config.
|
|
82
|
+
export function filterCategoriesByGatherMode(categories: LH.Config.ResolvedConfig['categories'], mode: LH.Gatherer.GatherMode): LH.Config.ResolvedConfig['categories'];
|
|
83
83
|
//# sourceMappingURL=filters.d.ts.map
|
package/core/config/filters.js
CHANGED
|
@@ -169,9 +169,9 @@ function filterAuditsByGatherMode(audits, mode) {
|
|
|
169
169
|
/**
|
|
170
170
|
* Optional `supportedModes` property can explicitly exclude a category even if some audits are available.
|
|
171
171
|
*
|
|
172
|
-
* @param {LH.Config.
|
|
172
|
+
* @param {LH.Config.ResolvedConfig['categories']} categories
|
|
173
173
|
* @param {LH.Gatherer.GatherMode} mode
|
|
174
|
-
* @return {LH.Config.
|
|
174
|
+
* @return {LH.Config.ResolvedConfig['categories']}
|
|
175
175
|
*/
|
|
176
176
|
function filterCategoriesByGatherMode(categories, mode) {
|
|
177
177
|
if (!categories) return null;
|
|
@@ -186,9 +186,9 @@ function filterCategoriesByGatherMode(categories, mode) {
|
|
|
186
186
|
/**
|
|
187
187
|
* Filters a categories object and their auditRefs down to the specified category ids.
|
|
188
188
|
*
|
|
189
|
-
* @param {LH.Config.
|
|
189
|
+
* @param {LH.Config.ResolvedConfig['categories']} categories
|
|
190
190
|
* @param {string[] | null | undefined} onlyCategories
|
|
191
|
-
* @return {LH.Config.
|
|
191
|
+
* @return {LH.Config.ResolvedConfig['categories']}
|
|
192
192
|
*/
|
|
193
193
|
function filterCategoriesByExplicitFilters(categories, onlyCategories) {
|
|
194
194
|
if (!categories || !onlyCategories) return categories;
|
|
@@ -202,7 +202,7 @@ function filterCategoriesByExplicitFilters(categories, onlyCategories) {
|
|
|
202
202
|
* Logs a warning if any specified onlyCategory is not a known category that can
|
|
203
203
|
* be included.
|
|
204
204
|
*
|
|
205
|
-
* @param {LH.Config.
|
|
205
|
+
* @param {LH.Config.ResolvedConfig['categories']} allCategories
|
|
206
206
|
* @param {string[] | null} onlyCategories
|
|
207
207
|
* @return {void}
|
|
208
208
|
*/
|
|
@@ -220,9 +220,9 @@ function warnOnUnknownOnlyCategories(allCategories, onlyCategories) {
|
|
|
220
220
|
* Filters a categories object and their auditRefs down to the set that can be computed using
|
|
221
221
|
* only the specified audits.
|
|
222
222
|
*
|
|
223
|
-
* @param {LH.Config.
|
|
223
|
+
* @param {LH.Config.ResolvedConfig['categories']} categories
|
|
224
224
|
* @param {Array<LH.Config.AuditDefn>} availableAudits
|
|
225
|
-
* @return {LH.Config.
|
|
225
|
+
* @return {LH.Config.ResolvedConfig['categories']}
|
|
226
226
|
*/
|
|
227
227
|
function filterCategoriesByAvailableAudits(categories, availableAudits) {
|
|
228
228
|
if (!categories) return categories;
|
|
@@ -223,6 +223,12 @@ function assertValidSettings(settings) {
|
|
|
223
223
|
throw new Error(`Screen emulation mobile setting (${settings.screenEmulation.mobile}) does not match formFactor setting (${settings.formFactor}). See https://github.com/GoogleChrome/lighthouse/blob/main/docs/emulation.md`);
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
+
|
|
227
|
+
const skippedAndOnlyAuditId =
|
|
228
|
+
settings.skipAudits?.find(auditId => settings.onlyAudits?.includes(auditId));
|
|
229
|
+
if (skippedAndOnlyAuditId) {
|
|
230
|
+
throw new Error(`${skippedAndOnlyAuditId} appears in both skipAudits and onlyAudits`);
|
|
231
|
+
}
|
|
226
232
|
}
|
|
227
233
|
|
|
228
234
|
/**
|
|
@@ -253,7 +259,13 @@ function assertArtifactTopologicalOrder(navigations) {
|
|
|
253
259
|
function assertValidConfig(resolvedConfig) {
|
|
254
260
|
const {warnings} = assertValidFRNavigations(resolvedConfig.navigations);
|
|
255
261
|
|
|
262
|
+
/** @type {Set<string>} */
|
|
263
|
+
const artifactIds = new Set();
|
|
256
264
|
for (const artifactDefn of resolvedConfig.artifacts || []) {
|
|
265
|
+
if (artifactIds.has(artifactDefn.id)) {
|
|
266
|
+
throw new Error(`Config defined multiple artifacts with id '${artifactDefn.id}'`);
|
|
267
|
+
}
|
|
268
|
+
artifactIds.add(artifactDefn.id);
|
|
257
269
|
assertValidFRGatherer(artifactDefn.gatherer);
|
|
258
270
|
}
|
|
259
271
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export default FRGatherer;
|
|
2
2
|
/**
|
|
3
|
-
* Base class for all gatherers
|
|
4
|
-
* Most extending classes should implement the Fraggle Rock API and let this class handle translation.
|
|
5
|
-
* See core/gather/gatherers/gatherer.js for legacy method explanations.
|
|
3
|
+
* Base class for all gatherers.
|
|
6
4
|
*
|
|
7
5
|
* @implements {LH.Gatherer.GathererInstance}
|
|
8
6
|
* @implements {LH.Gatherer.FRGathererInstance}
|
|
@@ -9,9 +9,7 @@ import * as LH from '../../types/lh.js';
|
|
|
9
9
|
/* eslint-disable no-unused-vars */
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Base class for all gatherers
|
|
13
|
-
* Most extending classes should implement the Fraggle Rock API and let this class handle translation.
|
|
14
|
-
* See core/gather/gatherers/gatherer.js for legacy method explanations.
|
|
12
|
+
* Base class for all gatherers.
|
|
15
13
|
*
|
|
16
14
|
* @implements {LH.Gatherer.GathererInstance}
|
|
17
15
|
* @implements {LH.Gatherer.FRGathererInstance}
|
|
@@ -41,18 +41,22 @@ async function runA11yChecks() {
|
|
|
41
41
|
// Consider http://go/prcpg for expert review of the aXe rules.
|
|
42
42
|
'accesskeys': {enabled: true},
|
|
43
43
|
'area-alt': {enabled: false},
|
|
44
|
+
'aria-dialog-name': {enabled: true},
|
|
44
45
|
'aria-roledescription': {enabled: false},
|
|
45
46
|
'aria-treeitem-name': {enabled: true},
|
|
47
|
+
'aria-text': {enabled: true},
|
|
46
48
|
'audio-caption': {enabled: false},
|
|
47
49
|
'blink': {enabled: false},
|
|
48
50
|
'duplicate-id': {enabled: false},
|
|
51
|
+
'empty-heading': {enabled: true},
|
|
49
52
|
'frame-focusable-content': {enabled: false},
|
|
50
53
|
'frame-title-unique': {enabled: false},
|
|
51
54
|
'heading-order': {enabled: true},
|
|
52
55
|
'html-xml-lang-mismatch': {enabled: true},
|
|
53
|
-
'identical-links-same-purpose': {enabled:
|
|
56
|
+
'identical-links-same-purpose': {enabled: true},
|
|
54
57
|
'input-button-name': {enabled: true},
|
|
55
|
-
'
|
|
58
|
+
'landmark-one-main': {enabled: true},
|
|
59
|
+
'link-in-text-block': {enabled: true},
|
|
56
60
|
'marquee': {enabled: false},
|
|
57
61
|
'meta-viewport': {enabled: true},
|
|
58
62
|
// https://github.com/dequelabs/axe-core/issues/2958
|
|
@@ -60,11 +64,12 @@ async function runA11yChecks() {
|
|
|
60
64
|
'no-autoplay-audio': {enabled: false},
|
|
61
65
|
'role-img-alt': {enabled: false},
|
|
62
66
|
'scrollable-region-focusable': {enabled: false},
|
|
63
|
-
'select-name': {enabled:
|
|
67
|
+
'select-name': {enabled: true},
|
|
64
68
|
'server-side-image-map': {enabled: false},
|
|
65
69
|
'svg-img-alt': {enabled: false},
|
|
66
70
|
'tabindex': {enabled: true},
|
|
67
71
|
'table-fake-caption': {enabled: true},
|
|
72
|
+
'target-size': {enabled: true},
|
|
68
73
|
'td-has-header': {enabled: true},
|
|
69
74
|
},
|
|
70
75
|
});
|
|
@@ -22,7 +22,6 @@ const MINIMAL_LEGIBLE_FONT_SIZE_PX = 12;
|
|
|
22
22
|
// limit number of protocol calls to make sure that gatherer doesn't take more than 1-2s
|
|
23
23
|
const MAX_NODES_SOURCE_RULE_FETCHED = 50; // number of nodes to fetch the source font-size rule
|
|
24
24
|
|
|
25
|
-
/** @typedef {import('../../../legacy/gather/driver.js')} Driver */
|
|
26
25
|
/** @typedef {LH.Artifacts.FontSize['analyzedFailingNodesData'][0]} NodeFontData */
|
|
27
26
|
/** @typedef {Map<number, {fontSize: number, textLength: number}>} BackendIdsToFontData */
|
|
28
27
|
|