lighthouse 12.8.2-dev.20251005 → 12.8.2-dev.20251007
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/config/exclusions.js +0 -2
- package/core/audits/audit.js +0 -1
- package/core/audits/insights/cls-culprits-insight.js +1 -1
- package/core/audits/insights/dom-size-insight.js +11 -7
- package/core/audits/insights/insight-audit.d.ts +4 -2
- package/core/audits/insights/insight-audit.js +22 -3
- package/core/audits/predictive-perf.js +2 -2
- package/core/audits/seo/crawlable-anchors.js +2 -3
- package/core/audits/server-response-time.d.ts +0 -5
- package/core/audits/server-response-time.js +12 -26
- package/core/computed/metrics/lcp-breakdown.d.ts +10 -5
- package/core/computed/metrics/lcp-breakdown.js +50 -22
- package/core/computed/metrics/time-to-first-byte.js +33 -10
- package/core/computed/metrics/timing-summary.js +3 -2
- package/core/config/default-config.js +20 -63
- package/core/config/experimental-config.js +1 -26
- package/core/config/filters.js +6 -9
- package/core/config/lr-desktop-config.js +0 -1
- package/core/config/lr-mobile-config.js +0 -1
- package/core/gather/gatherers/anchor-elements.js +8 -24
- package/core/gather/gatherers/inspector-issues.js +1 -28
- package/core/gather/gatherers/trace-elements.d.ts +0 -9
- package/core/gather/gatherers/trace-elements.js +0 -35
- package/core/lib/network-request.d.ts +0 -7
- package/core/lib/network-request.js +0 -16
- package/core/lib/proto-preprocessor.js +5 -22
- package/dist/report/bundle.esm.js +10 -49
- package/dist/report/flow.js +12 -51
- package/dist/report/standalone.js +11 -50
- package/flow-report/src/i18n/i18n.d.ts +4 -6
- package/package.json +4 -5
- package/report/assets/styles.css +0 -39
- package/report/renderer/api.js +0 -1
- package/report/renderer/category-renderer.js +6 -0
- package/report/renderer/components.js +1 -1
- package/report/renderer/dom.d.ts +0 -13
- package/report/renderer/dom.js +0 -38
- package/report/renderer/performance-category-renderer.d.ts +0 -26
- package/report/renderer/performance-category-renderer.js +10 -142
- package/report/renderer/report-ui-features.d.ts +0 -1
- package/report/renderer/report-ui-features.js +3 -13
- package/report/renderer/report-utils.d.ts +2 -3
- package/report/renderer/report-utils.js +4 -6
- package/report/types/report-renderer.d.ts +0 -6
- package/shared/localization/locales/ar-XB.json +20 -341
- package/shared/localization/locales/ar.json +20 -341
- package/shared/localization/locales/bg.json +9 -330
- package/shared/localization/locales/ca.json +9 -330
- package/shared/localization/locales/cs.json +9 -330
- package/shared/localization/locales/da.json +9 -330
- package/shared/localization/locales/de.json +9 -330
- package/shared/localization/locales/el.json +9 -330
- package/shared/localization/locales/en-GB.json +9 -330
- package/shared/localization/locales/en-US.json +44 -293
- package/shared/localization/locales/en-XA.json +0 -330
- package/shared/localization/locales/en-XL.json +44 -293
- package/shared/localization/locales/es-419.json +9 -330
- package/shared/localization/locales/es.json +9 -330
- package/shared/localization/locales/fi.json +9 -330
- package/shared/localization/locales/fil.json +9 -330
- package/shared/localization/locales/fr.json +9 -330
- package/shared/localization/locales/he.json +31 -352
- package/shared/localization/locales/hi.json +9 -330
- package/shared/localization/locales/hr.json +9 -330
- package/shared/localization/locales/hu.json +9 -330
- package/shared/localization/locales/id.json +9 -330
- package/shared/localization/locales/it.json +9 -330
- package/shared/localization/locales/ja.json +9 -330
- package/shared/localization/locales/ko.json +10 -331
- package/shared/localization/locales/lt.json +9 -330
- package/shared/localization/locales/lv.json +10 -331
- package/shared/localization/locales/nl.json +9 -330
- package/shared/localization/locales/no.json +9 -330
- package/shared/localization/locales/pl.json +9 -330
- package/shared/localization/locales/pt-PT.json +9 -330
- package/shared/localization/locales/pt.json +9 -330
- package/shared/localization/locales/ro.json +10 -331
- package/shared/localization/locales/ru.json +9 -330
- package/shared/localization/locales/sk.json +9 -330
- package/shared/localization/locales/sl.json +9 -330
- package/shared/localization/locales/sr-Latn.json +9 -330
- package/shared/localization/locales/sr.json +9 -330
- package/shared/localization/locales/sv.json +9 -330
- package/shared/localization/locales/ta.json +9 -330
- package/shared/localization/locales/te.json +10 -331
- package/shared/localization/locales/th.json +9 -330
- package/shared/localization/locales/tr.json +9 -330
- package/shared/localization/locales/uk.json +9 -330
- package/shared/localization/locales/vi.json +9 -330
- package/shared/localization/locales/zh-HK.json +9 -330
- package/shared/localization/locales/zh-TW.json +10 -331
- package/shared/localization/locales/zh.json +9 -330
- package/types/artifacts.d.ts +5 -6
- package/types/audit.d.ts +1 -1
- package/types/lhr/settings.d.ts +1 -1
- package/core/audits/byte-efficiency/duplicated-javascript.d.ts +0 -45
- package/core/audits/byte-efficiency/duplicated-javascript.js +0 -223
- package/core/audits/byte-efficiency/efficient-animated-content.d.ts +0 -22
- package/core/audits/byte-efficiency/efficient-animated-content.js +0 -93
- package/core/audits/byte-efficiency/legacy-javascript.d.ts +0 -28
- package/core/audits/byte-efficiency/legacy-javascript.js +0 -144
- package/core/audits/byte-efficiency/modern-image-formats.d.ts +0 -38
- package/core/audits/byte-efficiency/modern-image-formats.js +0 -187
- package/core/audits/byte-efficiency/render-blocking-resources.d.ts +0 -53
- package/core/audits/byte-efficiency/render-blocking-resources.js +0 -312
- package/core/audits/byte-efficiency/uses-long-cache-ttl.d.ts +0 -59
- package/core/audits/byte-efficiency/uses-long-cache-ttl.js +0 -293
- package/core/audits/byte-efficiency/uses-optimized-images.d.ts +0 -33
- package/core/audits/byte-efficiency/uses-optimized-images.js +0 -146
- package/core/audits/byte-efficiency/uses-responsive-images-snapshot.d.ts +0 -16
- package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +0 -106
- package/core/audits/byte-efficiency/uses-responsive-images.d.ts +0 -44
- package/core/audits/byte-efficiency/uses-responsive-images.js +0 -202
- package/core/audits/byte-efficiency/uses-text-compression.d.ts +0 -14
- package/core/audits/byte-efficiency/uses-text-compression.js +0 -108
- package/core/audits/critical-request-chains.d.ts +0 -44
- package/core/audits/critical-request-chains.js +0 -221
- package/core/audits/dobetterweb/dom-size.d.ts +0 -32
- package/core/audits/dobetterweb/dom-size.js +0 -182
- package/core/audits/dobetterweb/uses-http2.d.ts +0 -72
- package/core/audits/dobetterweb/uses-http2.js +0 -276
- package/core/audits/font-display.d.ts +0 -32
- package/core/audits/font-display.js +0 -195
- package/core/audits/largest-contentful-paint-element.d.ts +0 -34
- package/core/audits/largest-contentful-paint-element.js +0 -181
- package/core/audits/lcp-lazy-loaded.d.ts +0 -22
- package/core/audits/lcp-lazy-loaded.js +0 -115
- package/core/audits/prioritize-lcp-image.d.ts +0 -74
- package/core/audits/prioritize-lcp-image.js +0 -297
- package/core/audits/third-party-summary.d.ts +0 -78
- package/core/audits/third-party-summary.js +0 -236
- package/core/audits/uses-rel-preconnect.d.ts +0 -37
- package/core/audits/uses-rel-preconnect.js +0 -286
- package/core/audits/viewport.d.ts +0 -17
- package/core/audits/viewport.js +0 -87
- package/core/audits/work-during-interaction.d.ts +0 -81
- package/core/audits/work-during-interaction.js +0 -287
- package/core/computed/critical-request-chains.d.ts +0 -42
- package/core/computed/critical-request-chains.js +0 -143
- package/core/computed/viewport-meta.d.ts +0 -37
- package/core/computed/viewport-meta.js +0 -71
- package/types/internal/metaviewport-parser.d.ts +0 -13
|
@@ -36,8 +36,6 @@ const exclusions = {
|
|
|
36
36
|
for (const array of Object.values(exclusions)) {
|
|
37
37
|
// https://github.com/GoogleChrome/lighthouse/issues/14271
|
|
38
38
|
array.push('lantern-idle-callback-short');
|
|
39
|
-
// https://github.com/GoogleChrome/lighthouse/issues/16597
|
|
40
|
-
array.push('csp-block-all');
|
|
41
39
|
// glitch is gone.
|
|
42
40
|
array.push('issues-mixed-content');
|
|
43
41
|
// works most of the time, but since it uses a live site it can be flaky
|
package/core/audits/audit.js
CHANGED
|
@@ -38,7 +38,7 @@ class CLSCulpritsInsight extends Audit {
|
|
|
38
38
|
description: insightStr_(InsightUIStrings.description),
|
|
39
39
|
guidanceLevel: 3,
|
|
40
40
|
requiredArtifacts: ['Trace', 'TraceElements', 'SourceMaps'],
|
|
41
|
-
replacesAudits: ['layout-shifts'
|
|
41
|
+
replacesAudits: ['layout-shifts'],
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -59,21 +59,21 @@ class DOMSizeInsight extends Audit {
|
|
|
59
59
|
},
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
|
-
statistic: str_(UIStrings.
|
|
63
|
-
node: makeNodeItemForNodeId(artifacts.TraceElements,
|
|
62
|
+
statistic: str_(UIStrings.maxDOMDepth),
|
|
63
|
+
node: makeNodeItemForNodeId(artifacts.TraceElements, maxDepth.nodeId),
|
|
64
64
|
value: {
|
|
65
65
|
type: 'numeric',
|
|
66
66
|
granularity: 1,
|
|
67
|
-
value:
|
|
67
|
+
value: maxDepth.depth,
|
|
68
68
|
},
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
|
-
statistic: str_(UIStrings.
|
|
72
|
-
node: makeNodeItemForNodeId(artifacts.TraceElements,
|
|
71
|
+
statistic: str_(UIStrings.maxChildren),
|
|
72
|
+
node: makeNodeItemForNodeId(artifacts.TraceElements, maxChildren.nodeId),
|
|
73
73
|
value: {
|
|
74
74
|
type: 'numeric',
|
|
75
75
|
granularity: 1,
|
|
76
|
-
value:
|
|
76
|
+
value: maxChildren.numChildren,
|
|
77
77
|
},
|
|
78
78
|
},
|
|
79
79
|
];
|
|
@@ -85,7 +85,11 @@ class DOMSizeInsight extends Audit {
|
|
|
85
85
|
maxChildren: maxChildren.numChildren,
|
|
86
86
|
maxDepth: maxDepth.depth,
|
|
87
87
|
};
|
|
88
|
-
return
|
|
88
|
+
return {
|
|
89
|
+
details,
|
|
90
|
+
numericValue: totalElements,
|
|
91
|
+
numericUnit: 'element',
|
|
92
|
+
};
|
|
89
93
|
});
|
|
90
94
|
}
|
|
91
95
|
}
|
|
@@ -11,13 +11,15 @@ export type CreateDetailsExtras = {
|
|
|
11
11
|
* @param {LH.Artifacts} artifacts
|
|
12
12
|
* @param {LH.Audit.Context} context
|
|
13
13
|
* @param {T} insightName
|
|
14
|
-
* @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings
|
|
14
|
+
* @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings?: Array<string | LH.IcuMessage>, numericValue?: number, numericUnit?: LH.Audit.NumericProduct['numericUnit']}|LH.Audit.Details|undefined} createDetails
|
|
15
15
|
* @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
|
|
16
16
|
* @return {Promise<LH.Audit.Product>}
|
|
17
17
|
*/
|
|
18
18
|
export function adaptInsightToAuditProduct<T extends keyof import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModelsType>(artifacts: LH.Artifacts, context: LH.Audit.Context, insightName: T, createDetails: (insight: import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModels[T], extras: CreateDetailsExtras) => {
|
|
19
19
|
details: LH.Audit.Details;
|
|
20
|
-
warnings
|
|
20
|
+
warnings?: Array<string | LH.IcuMessage>;
|
|
21
|
+
numericValue?: number;
|
|
22
|
+
numericUnit?: LH.Audit.NumericProduct["numericUnit"];
|
|
21
23
|
} | LH.Audit.Details | undefined): Promise<LH.Audit.Product>;
|
|
22
24
|
/**
|
|
23
25
|
* @param {LH.Artifacts.TraceElement[]} traceElements
|
|
@@ -42,7 +42,7 @@ async function getInsightSet(artifacts, context) {
|
|
|
42
42
|
* @param {LH.Artifacts} artifacts
|
|
43
43
|
* @param {LH.Audit.Context} context
|
|
44
44
|
* @param {T} insightName
|
|
45
|
-
* @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings
|
|
45
|
+
* @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings?: Array<string | LH.IcuMessage>, numericValue?: number, numericUnit?: LH.Audit.NumericProduct['numericUnit']}|LH.Audit.Details|undefined} createDetails
|
|
46
46
|
* @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
|
|
47
47
|
* @return {Promise<LH.Audit.Product>}
|
|
48
48
|
*/
|
|
@@ -70,11 +70,17 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
const warnings = [...insight.warnings ?? []];
|
|
73
|
+
/** @type {number|undefined} */
|
|
74
|
+
let numericValue;
|
|
75
|
+
/** @type {LH.Audit.NumericProduct['numericUnit']|undefined} */
|
|
76
|
+
let numericUnit;
|
|
73
77
|
|
|
74
78
|
let details;
|
|
75
|
-
if (cbResult && '
|
|
79
|
+
if (cbResult && 'details' in cbResult) {
|
|
76
80
|
details = cbResult.details;
|
|
77
|
-
warnings.push(...cbResult.warnings);
|
|
81
|
+
if (cbResult.warnings) warnings.push(...cbResult.warnings);
|
|
82
|
+
numericValue = cbResult.numericValue;
|
|
83
|
+
numericUnit = cbResult.numericUnit;
|
|
78
84
|
} else {
|
|
79
85
|
details = cbResult;
|
|
80
86
|
}
|
|
@@ -163,6 +169,19 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
|
|
|
163
169
|
scoreDisplayMode = Audit.SCORING_MODES.INFORMATIVE;
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
if (numericValue !== undefined && numericUnit !== undefined) {
|
|
173
|
+
return {
|
|
174
|
+
scoreDisplayMode,
|
|
175
|
+
score,
|
|
176
|
+
numericValue,
|
|
177
|
+
numericUnit,
|
|
178
|
+
metricSavings,
|
|
179
|
+
warnings: warnings.length ? warnings : undefined,
|
|
180
|
+
displayValue,
|
|
181
|
+
details,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
166
185
|
return {
|
|
167
186
|
scoreDisplayMode,
|
|
168
187
|
score,
|
|
@@ -76,8 +76,8 @@ class PredictivePerf extends Audit {
|
|
|
76
76
|
pessimisticLCP: lcp.pessimisticEstimate.timeInMs,
|
|
77
77
|
|
|
78
78
|
roughEstimateOfTTFB: timingSummary.metrics.timeToFirstByte,
|
|
79
|
-
roughEstimateOfLCPLoadStart: timingSummary.metrics.
|
|
80
|
-
roughEstimateOfLCPLoadEnd: timingSummary.metrics.
|
|
79
|
+
roughEstimateOfLCPLoadStart: timingSummary.metrics.lcpLoadDelay,
|
|
80
|
+
roughEstimateOfLCPLoadEnd: timingSummary.metrics.lcpLoadDuration,
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
const score = Audit.computeLogNormalScore(
|
|
@@ -57,12 +57,10 @@ class CrawlableAnchors extends Audit {
|
|
|
57
57
|
href,
|
|
58
58
|
attributeNames = [],
|
|
59
59
|
listeners = [],
|
|
60
|
-
ancestorListeners = [],
|
|
61
60
|
}) => {
|
|
62
61
|
rawHref = rawHref.replace( /\s/g, '');
|
|
63
62
|
name = name.trim();
|
|
64
63
|
role = role.trim();
|
|
65
|
-
const hasListener = Boolean(listeners.length || ancestorListeners.length);
|
|
66
64
|
|
|
67
65
|
if (role.length > 0) return;
|
|
68
66
|
// Ignore mailto links even if they use one of the failing patterns. See https://github.com/GoogleChrome/lighthouse/issues/11443#issuecomment-694898412
|
|
@@ -86,7 +84,8 @@ class CrawlableAnchors extends Audit {
|
|
|
86
84
|
!attributeNames.includes('href') &&
|
|
87
85
|
hrefAssociatedAttributes.every(attribute => !attributeNames.includes(attribute))
|
|
88
86
|
) {
|
|
89
|
-
|
|
87
|
+
// If it has an even listener (e.g. onclick) then we can't assume it's a placeholder. Therefore we consider it failing.
|
|
88
|
+
return Boolean(listeners.length);
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
if (href === '') return true;
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
export default ServerResponseTime;
|
|
2
2
|
declare class ServerResponseTime extends Audit {
|
|
3
|
-
/**
|
|
4
|
-
* @param {LH.Artifacts.NetworkRequest} record
|
|
5
|
-
* @return {number|null}
|
|
6
|
-
*/
|
|
7
|
-
static calculateResponseTime(record: LH.Artifacts.NetworkRequest): number | null;
|
|
8
3
|
/**
|
|
9
4
|
* @param {LH.Artifacts} artifacts
|
|
10
5
|
* @param {LH.Audit.Context} context
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {Audit} from './audit.js';
|
|
8
8
|
import * as i18n from '../lib/i18n/i18n.js';
|
|
9
|
-
import {
|
|
9
|
+
import {NavigationInsights} from '../computed/navigation-insights.js';
|
|
10
10
|
|
|
11
11
|
const UIStrings = {
|
|
12
12
|
/** Title of a diagnostic audit that provides detail on how long it took from starting a request to when the server started responding. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
|
|
@@ -21,9 +21,6 @@ const UIStrings = {
|
|
|
21
21
|
|
|
22
22
|
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
23
23
|
|
|
24
|
-
// Due to the way that DevTools throttling works we cannot see if server response took less than ~570ms.
|
|
25
|
-
// We set our failure threshold to 600ms to avoid those false positives but we want devs to shoot for 100ms.
|
|
26
|
-
const TOO_SLOW_THRESHOLD_MS = 600;
|
|
27
24
|
const TARGET_MS = 100;
|
|
28
25
|
|
|
29
26
|
class ServerResponseTime extends Audit {
|
|
@@ -38,41 +35,30 @@ class ServerResponseTime extends Audit {
|
|
|
38
35
|
description: str_(UIStrings.description),
|
|
39
36
|
supportedModes: ['navigation'],
|
|
40
37
|
guidanceLevel: 1,
|
|
41
|
-
requiredArtifacts: ['
|
|
38
|
+
requiredArtifacts: ['Trace', 'SourceMaps'],
|
|
42
39
|
scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
|
|
43
40
|
};
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
/**
|
|
47
|
-
* @param {LH.Artifacts.NetworkRequest} record
|
|
48
|
-
* @return {number|null}
|
|
49
|
-
*/
|
|
50
|
-
static calculateResponseTime(record) {
|
|
51
|
-
// Lightrider does not have timings for sendEnd, but we do have this timing which should be
|
|
52
|
-
// close to the response time.
|
|
53
|
-
if (global.isLightrider && record.lrStatistics) return record.lrStatistics.requestMs;
|
|
54
|
-
|
|
55
|
-
if (!record.timing) return null;
|
|
56
|
-
return record.timing.receiveHeadersStart - record.timing.sendEnd;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
43
|
/**
|
|
60
44
|
* @param {LH.Artifacts} artifacts
|
|
61
45
|
* @param {LH.Audit.Context} context
|
|
62
46
|
* @return {Promise<LH.Audit.Product>}
|
|
63
47
|
*/
|
|
64
48
|
static async audit(artifacts, context) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
49
|
+
const settings = context.settings;
|
|
50
|
+
const trace = artifacts.Trace;
|
|
51
|
+
const SourceMaps = artifacts.SourceMaps;
|
|
52
|
+
const navInsights = await NavigationInsights.request({trace, settings, SourceMaps}, context);
|
|
53
|
+
const responseTime = navInsights.model.DocumentLatency.data?.serverResponseTime;
|
|
54
|
+
const url = navInsights.model.DocumentLatency.data?.documentRequest?.args.data.url;
|
|
69
55
|
|
|
70
|
-
|
|
71
|
-
if (responseTime === null) {
|
|
56
|
+
if (responseTime === undefined || !url) {
|
|
72
57
|
throw new Error('no timing found for main resource');
|
|
73
58
|
}
|
|
74
59
|
|
|
75
|
-
const passed =
|
|
60
|
+
const passed =
|
|
61
|
+
Boolean(navInsights.model.DocumentLatency.data?.checklist.serverResponseIsFast.value);
|
|
76
62
|
const displayValue = str_(UIStrings.displayValue, {timeInMs: responseTime});
|
|
77
63
|
|
|
78
64
|
/** @type {LH.Audit.Details.Opportunity['headings']} */
|
|
@@ -84,7 +70,7 @@ class ServerResponseTime extends Audit {
|
|
|
84
70
|
const overallSavingsMs = Math.max(responseTime - TARGET_MS, 0);
|
|
85
71
|
const details = Audit.makeOpportunityDetails(
|
|
86
72
|
headings,
|
|
87
|
-
[{url
|
|
73
|
+
[{url, responseTime}],
|
|
88
74
|
{overallSavingsMs}
|
|
89
75
|
);
|
|
90
76
|
|
|
@@ -2,20 +2,25 @@ export { LCPBreakdownComputed as LCPBreakdown };
|
|
|
2
2
|
declare const LCPBreakdownComputed: typeof LCPBreakdown & {
|
|
3
3
|
request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext) => Promise<{
|
|
4
4
|
ttfb: number;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
loadDelay?: number;
|
|
6
|
+
loadDuration?: number;
|
|
7
|
+
renderDelay?: number;
|
|
7
8
|
}>;
|
|
8
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Note: this omits renderDelay for simulated throttling.
|
|
12
|
+
*/
|
|
9
13
|
declare class LCPBreakdown {
|
|
10
14
|
/**
|
|
11
15
|
* @param {LH.Artifacts.MetricComputationDataInput} data
|
|
12
16
|
* @param {LH.Artifacts.ComputedContext} context
|
|
13
|
-
* @return {Promise<{ttfb: number,
|
|
17
|
+
* @return {Promise<{ttfb: number, loadDelay?: number, loadDuration?: number, renderDelay?: number}>}
|
|
14
18
|
*/
|
|
15
19
|
static compute_(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
|
|
16
20
|
ttfb: number;
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
loadDelay?: number;
|
|
22
|
+
loadDuration?: number;
|
|
23
|
+
renderDelay?: number;
|
|
19
24
|
}>;
|
|
20
25
|
}
|
|
21
26
|
//# sourceMappingURL=lcp-breakdown.d.ts.map
|
|
@@ -10,42 +10,70 @@ import {LargestContentfulPaint} from './largest-contentful-paint.js';
|
|
|
10
10
|
import {ProcessedNavigation} from '../processed-navigation.js';
|
|
11
11
|
import {TimeToFirstByte} from './time-to-first-byte.js';
|
|
12
12
|
import {LCPImageRecord} from '../lcp-image-record.js';
|
|
13
|
+
import {NavigationInsights} from '../navigation-insights.js';
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Note: this omits renderDelay for simulated throttling.
|
|
17
|
+
*/
|
|
14
18
|
class LCPBreakdown {
|
|
15
19
|
/**
|
|
16
20
|
* @param {LH.Artifacts.MetricComputationDataInput} data
|
|
17
21
|
* @param {LH.Artifacts.ComputedContext} context
|
|
18
|
-
* @return {Promise<{ttfb: number,
|
|
22
|
+
* @return {Promise<{ttfb: number, loadDelay?: number, loadDuration?: number, renderDelay?: number}>}
|
|
19
23
|
*/
|
|
20
24
|
static async compute_(data, context) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
if (data.settings.throttlingMethod === 'simulate') {
|
|
26
|
+
const processedNavigation = await ProcessedNavigation.request(data.trace, context);
|
|
27
|
+
const observedLcp = processedNavigation.timings.largestContentfulPaint;
|
|
28
|
+
if (observedLcp === undefined) {
|
|
29
|
+
throw new LighthouseError(LighthouseError.errors.NO_LCP);
|
|
30
|
+
}
|
|
31
|
+
const timeOrigin = processedNavigation.timestamps.timeOrigin / 1000;
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
const {timing: ttfb} = await TimeToFirstByte.request(data, context);
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const lcpRecord = await LCPImageRecord.request(data, context);
|
|
36
|
+
if (!lcpRecord) {
|
|
37
|
+
return {ttfb};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Official LCP^tm. Will be lantern result if simulated, otherwise same as observedLcp.
|
|
41
|
+
const {timing: metricLcp} = await LargestContentfulPaint.request(data, context);
|
|
42
|
+
const throttleRatio = metricLcp / observedLcp;
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const throttleRatio = metricLcp / observedLcp;
|
|
44
|
+
const unclampedLoadStart = (lcpRecord.networkRequestTime - timeOrigin) * throttleRatio;
|
|
45
|
+
const loadDelay = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
const unclampedLoadEnd = (lcpRecord.networkEndTime - timeOrigin) * throttleRatio;
|
|
48
|
+
const loadDuration = Math.max(loadDelay, Math.min(unclampedLoadEnd, metricLcp));
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
return {
|
|
51
|
+
ttfb,
|
|
52
|
+
loadDelay,
|
|
53
|
+
loadDuration,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const {trace, settings, SourceMaps} = data;
|
|
58
|
+
const navInsights = await NavigationInsights.request({trace, settings, SourceMaps}, context);
|
|
59
|
+
const lcpBreakdown = navInsights.model.LCPBreakdown;
|
|
60
|
+
if (lcpBreakdown instanceof Error) {
|
|
61
|
+
throw new LighthouseError(LighthouseError.errors.NO_LCP, {}, {cause: lcpBreakdown});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!lcpBreakdown.subparts) {
|
|
65
|
+
throw new LighthouseError(LighthouseError.errors.NO_LCP);
|
|
66
|
+
}
|
|
44
67
|
|
|
45
68
|
return {
|
|
46
|
-
ttfb,
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
ttfb: lcpBreakdown.subparts.ttfb.range / 1000,
|
|
70
|
+
loadDelay: lcpBreakdown.subparts.loadDelay !== undefined ?
|
|
71
|
+
lcpBreakdown.subparts.loadDelay.range / 1000 :
|
|
72
|
+
undefined,
|
|
73
|
+
loadDuration: lcpBreakdown.subparts.loadDuration !== undefined ?
|
|
74
|
+
lcpBreakdown.subparts.loadDuration.range / 1000 :
|
|
75
|
+
undefined,
|
|
76
|
+
renderDelay: lcpBreakdown.subparts.renderDelay.range / 1000,
|
|
49
77
|
};
|
|
50
78
|
}
|
|
51
79
|
}
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import {calculateDocFirstByteTs} from '@paulirish/trace_engine/models/trace/insights/Common.js';
|
|
8
|
+
|
|
7
9
|
import {makeComputedArtifact} from '../computed-artifact.js';
|
|
8
10
|
import {NavigationMetric} from './navigation-metric.js';
|
|
9
11
|
import {MainResource} from '../main-resource.js';
|
|
10
12
|
import {NetworkAnalysis} from '../network-analysis.js';
|
|
13
|
+
import {NavigationInsights} from '../navigation-insights.js';
|
|
14
|
+
import {TraceEngineResult} from '../trace-engine-result.js';
|
|
11
15
|
|
|
12
16
|
class TimeToFirstByte extends NavigationMetric {
|
|
13
17
|
/**
|
|
@@ -41,18 +45,37 @@ class TimeToFirstByte extends NavigationMetric {
|
|
|
41
45
|
* @return {Promise<LH.Artifacts.Metric>}
|
|
42
46
|
*/
|
|
43
47
|
static async computeObservedMetric(data, context) {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
const {trace, settings, SourceMaps} = data;
|
|
49
|
+
const traceEngineResult =
|
|
50
|
+
await TraceEngineResult.request({trace, settings, SourceMaps}, context);
|
|
51
|
+
const navInsights = await NavigationInsights.request({trace, settings, SourceMaps}, context);
|
|
52
|
+
const lcpBreakdown = navInsights.model.LCPBreakdown;
|
|
53
|
+
|
|
54
|
+
// Defer to LCP breakdown, but if there's no LCP fallback to manual calculation.
|
|
55
|
+
if (!(lcpBreakdown instanceof Error) && lcpBreakdown.subparts) {
|
|
56
|
+
return {
|
|
57
|
+
timing: lcpBreakdown.subparts.ttfb.range / 1000,
|
|
58
|
+
timestamp: lcpBreakdown.subparts.ttfb.max,
|
|
59
|
+
};
|
|
60
|
+
} else if (navInsights.navigation?.args.data?.navigationId) {
|
|
61
|
+
const request = traceEngineResult.data.NetworkRequests.byId.get(
|
|
62
|
+
navInsights.navigation.args.data.navigationId);
|
|
63
|
+
if (!request) {
|
|
64
|
+
throw new Error();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const timestamp = calculateDocFirstByteTs(request);
|
|
68
|
+
if (timestamp === null) {
|
|
69
|
+
throw new Error('cannot calculate ttfb');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
timing: (timestamp - navInsights.navigation.ts) / 1000,
|
|
74
|
+
timestamp,
|
|
75
|
+
};
|
|
47
76
|
}
|
|
48
77
|
|
|
49
|
-
|
|
50
|
-
const timeOriginTs = processedNavigation.timestamps.timeOrigin;
|
|
51
|
-
const timestampMs =
|
|
52
|
-
mainResource.timing.requestTime * 1000 + mainResource.timing.receiveHeadersStart;
|
|
53
|
-
const timestamp = timestampMs * 1000;
|
|
54
|
-
const timing = (timestamp - timeOriginTs) / 1000;
|
|
55
|
-
return {timing, timestamp};
|
|
78
|
+
throw new Error('cannot determine ttfb');
|
|
56
79
|
}
|
|
57
80
|
}
|
|
58
81
|
|
|
@@ -96,8 +96,9 @@ class TimingSummary {
|
|
|
96
96
|
cumulativeLayoutShift,
|
|
97
97
|
cumulativeLayoutShiftMainFrame,
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
lcpLoadDelay: lcpBreakdown?.loadDelay,
|
|
100
|
+
lcpLoadDuration: lcpBreakdown?.loadDuration,
|
|
101
|
+
lcpRenderDelay: lcpBreakdown?.renderDelay,
|
|
101
102
|
|
|
102
103
|
timeToFirstByte: ttfb?.timing,
|
|
103
104
|
timeToFirstByteTs: ttfb?.timestamp,
|
|
@@ -147,7 +147,6 @@ const defaultConfig = {
|
|
|
147
147
|
audits: [
|
|
148
148
|
'is-on-https',
|
|
149
149
|
'redirects-http',
|
|
150
|
-
'viewport',
|
|
151
150
|
'metrics/first-contentful-paint',
|
|
152
151
|
'metrics/largest-contentful-paint',
|
|
153
152
|
'metrics/first-meaningful-paint',
|
|
@@ -162,7 +161,6 @@ const defaultConfig = {
|
|
|
162
161
|
'server-response-time',
|
|
163
162
|
'metrics/interactive',
|
|
164
163
|
'user-timings',
|
|
165
|
-
'critical-request-chains',
|
|
166
164
|
'redirects',
|
|
167
165
|
'image-aspect-ratio',
|
|
168
166
|
'image-size-responsive',
|
|
@@ -170,8 +168,6 @@ const defaultConfig = {
|
|
|
170
168
|
'third-party-cookies',
|
|
171
169
|
'mainthread-work-breakdown',
|
|
172
170
|
'bootup-time',
|
|
173
|
-
'uses-rel-preconnect',
|
|
174
|
-
'font-display',
|
|
175
171
|
'diagnostics',
|
|
176
172
|
'network-requests',
|
|
177
173
|
'network-rtt',
|
|
@@ -179,15 +175,11 @@ const defaultConfig = {
|
|
|
179
175
|
'main-thread-tasks',
|
|
180
176
|
'metrics',
|
|
181
177
|
'resource-summary',
|
|
182
|
-
'third-party-summary',
|
|
183
|
-
'largest-contentful-paint-element',
|
|
184
|
-
'lcp-lazy-loaded',
|
|
185
178
|
'layout-shifts',
|
|
186
179
|
'long-tasks',
|
|
187
180
|
'non-composited-animations',
|
|
188
181
|
'unsized-images',
|
|
189
182
|
'valid-source-maps',
|
|
190
|
-
'prioritize-lcp-image',
|
|
191
183
|
'csp-xss',
|
|
192
184
|
'has-hsts',
|
|
193
185
|
'origin-isolation',
|
|
@@ -267,32 +259,20 @@ const defaultConfig = {
|
|
|
267
259
|
'accessibility/manual/offscreen-content-hidden',
|
|
268
260
|
'accessibility/manual/use-landmarks',
|
|
269
261
|
'accessibility/manual/visual-order-follows-dom',
|
|
270
|
-
'byte-efficiency/uses-long-cache-ttl',
|
|
271
262
|
'byte-efficiency/total-byte-weight',
|
|
272
263
|
'byte-efficiency/offscreen-images',
|
|
273
|
-
'byte-efficiency/render-blocking-resources',
|
|
274
264
|
'byte-efficiency/unminified-css',
|
|
275
265
|
'byte-efficiency/unminified-javascript',
|
|
276
266
|
'byte-efficiency/unused-css-rules',
|
|
277
267
|
'byte-efficiency/unused-javascript',
|
|
278
|
-
'byte-efficiency/modern-image-formats',
|
|
279
|
-
'byte-efficiency/uses-optimized-images',
|
|
280
|
-
'byte-efficiency/uses-text-compression',
|
|
281
|
-
'byte-efficiency/uses-responsive-images',
|
|
282
|
-
'byte-efficiency/efficient-animated-content',
|
|
283
|
-
'byte-efficiency/duplicated-javascript',
|
|
284
|
-
'byte-efficiency/legacy-javascript',
|
|
285
|
-
'byte-efficiency/uses-responsive-images-snapshot',
|
|
286
268
|
'dobetterweb/doctype',
|
|
287
269
|
'dobetterweb/charset',
|
|
288
|
-
'dobetterweb/dom-size',
|
|
289
270
|
'dobetterweb/geolocation-on-start',
|
|
290
271
|
'dobetterweb/inspector-issues',
|
|
291
272
|
'dobetterweb/no-document-write',
|
|
292
273
|
'dobetterweb/js-libraries',
|
|
293
274
|
'dobetterweb/notification-on-start',
|
|
294
275
|
'dobetterweb/paste-preventing-inputs',
|
|
295
|
-
'dobetterweb/uses-http2',
|
|
296
276
|
'dobetterweb/uses-passive-event-listeners',
|
|
297
277
|
'seo/meta-description',
|
|
298
278
|
'seo/http-status-code',
|
|
@@ -303,7 +283,6 @@ const defaultConfig = {
|
|
|
303
283
|
'seo/hreflang',
|
|
304
284
|
'seo/canonical',
|
|
305
285
|
'seo/manual/structured-data',
|
|
306
|
-
'work-during-interaction',
|
|
307
286
|
'bf-cache',
|
|
308
287
|
'insights/cache-insight',
|
|
309
288
|
'insights/cls-culprits-insight',
|
|
@@ -407,67 +386,43 @@ const defaultConfig = {
|
|
|
407
386
|
{id: 'interaction-to-next-paint', weight: 0, group: 'metrics', acronym: 'INP'},
|
|
408
387
|
|
|
409
388
|
// Insight audits.
|
|
410
|
-
{id: 'cache-insight', weight: 0, group: '
|
|
411
|
-
{id: 'cls-culprits-insight', weight: 0, group: '
|
|
412
|
-
{id: 'document-latency-insight', weight: 0, group: '
|
|
413
|
-
{id: 'dom-size-insight', weight: 0, group: '
|
|
414
|
-
{id: 'duplicated-javascript-insight', weight: 0, group: '
|
|
415
|
-
{id: 'font-display-insight', weight: 0, group: '
|
|
416
|
-
{id: 'forced-reflow-insight', weight: 0, group: '
|
|
417
|
-
{id: 'image-delivery-insight', weight: 0, group: '
|
|
418
|
-
{id: 'inp-breakdown-insight', weight: 0, group: '
|
|
419
|
-
{id: 'lcp-breakdown-insight', weight: 0, group: '
|
|
420
|
-
{id: 'lcp-discovery-insight', weight: 0, group: '
|
|
421
|
-
{id: 'legacy-javascript-insight', weight: 0, group: '
|
|
422
|
-
{id: 'modern-http-insight', weight: 0, group: '
|
|
423
|
-
{id: 'network-dependency-tree-insight', weight: 0, group: '
|
|
424
|
-
{id: 'render-blocking-insight', weight: 0, group: '
|
|
425
|
-
{id: 'third-parties-insight', weight: 0, group: '
|
|
426
|
-
{id: 'viewport-insight', weight: 0, group: '
|
|
389
|
+
{id: 'cache-insight', weight: 0, group: 'insights'},
|
|
390
|
+
{id: 'cls-culprits-insight', weight: 0, group: 'insights'},
|
|
391
|
+
{id: 'document-latency-insight', weight: 0, group: 'insights'},
|
|
392
|
+
{id: 'dom-size-insight', weight: 0, group: 'insights'},
|
|
393
|
+
{id: 'duplicated-javascript-insight', weight: 0, group: 'insights'},
|
|
394
|
+
{id: 'font-display-insight', weight: 0, group: 'insights'},
|
|
395
|
+
{id: 'forced-reflow-insight', weight: 0, group: 'insights'},
|
|
396
|
+
{id: 'image-delivery-insight', weight: 0, group: 'insights'},
|
|
397
|
+
{id: 'inp-breakdown-insight', weight: 0, group: 'insights'},
|
|
398
|
+
{id: 'lcp-breakdown-insight', weight: 0, group: 'insights'},
|
|
399
|
+
{id: 'lcp-discovery-insight', weight: 0, group: 'insights'},
|
|
400
|
+
{id: 'legacy-javascript-insight', weight: 0, group: 'insights'},
|
|
401
|
+
{id: 'modern-http-insight', weight: 0, group: 'insights'},
|
|
402
|
+
{id: 'network-dependency-tree-insight', weight: 0, group: 'insights'},
|
|
403
|
+
{id: 'render-blocking-insight', weight: 0, group: 'insights'},
|
|
404
|
+
{id: 'third-parties-insight', weight: 0, group: 'insights'},
|
|
405
|
+
{id: 'viewport-insight', weight: 0, group: 'insights'},
|
|
427
406
|
|
|
428
407
|
// These are our "invisible" metrics. Not displayed, but still in the LHR.
|
|
429
408
|
{id: 'interactive', weight: 0, group: 'hidden', acronym: 'TTI'},
|
|
430
409
|
{id: 'max-potential-fid', weight: 0, group: 'hidden'},
|
|
431
410
|
{id: 'first-meaningful-paint', weight: 0, acronym: 'FMP', group: 'hidden'},
|
|
432
411
|
|
|
433
|
-
{id: 'render-blocking-resources', weight: 0, group: 'diagnostics'},
|
|
434
|
-
{id: 'uses-responsive-images', weight: 0, group: 'diagnostics'},
|
|
435
412
|
{id: 'offscreen-images', weight: 0, group: 'diagnostics'},
|
|
436
413
|
{id: 'unminified-css', weight: 0, group: 'diagnostics'},
|
|
437
414
|
{id: 'unminified-javascript', weight: 0, group: 'diagnostics'},
|
|
438
415
|
{id: 'unused-css-rules', weight: 0, group: 'diagnostics'},
|
|
439
416
|
{id: 'unused-javascript', weight: 0, group: 'diagnostics'},
|
|
440
|
-
{id: 'uses-optimized-images', weight: 0, group: 'diagnostics'},
|
|
441
|
-
{id: 'modern-image-formats', weight: 0, group: 'diagnostics'},
|
|
442
|
-
{id: 'uses-text-compression', weight: 0, group: 'diagnostics'},
|
|
443
|
-
{id: 'uses-rel-preconnect', weight: 0, group: 'diagnostics'},
|
|
444
|
-
{id: 'server-response-time', weight: 0, group: 'diagnostics'},
|
|
445
|
-
{id: 'redirects', weight: 0, group: 'diagnostics'},
|
|
446
|
-
{id: 'uses-http2', weight: 0, group: 'diagnostics'},
|
|
447
|
-
{id: 'efficient-animated-content', weight: 0, group: 'diagnostics'},
|
|
448
|
-
{id: 'duplicated-javascript', weight: 0, group: 'diagnostics'},
|
|
449
|
-
{id: 'legacy-javascript', weight: 0, group: 'diagnostics'},
|
|
450
|
-
{id: 'prioritize-lcp-image', weight: 0, group: 'diagnostics'},
|
|
451
417
|
{id: 'total-byte-weight', weight: 0, group: 'diagnostics'},
|
|
452
|
-
{id: 'uses-long-cache-ttl', weight: 0, group: 'diagnostics'},
|
|
453
|
-
{id: 'dom-size', weight: 0, group: 'diagnostics'},
|
|
454
|
-
{id: 'critical-request-chains', weight: 0, group: 'diagnostics'},
|
|
455
418
|
{id: 'user-timings', weight: 0, group: 'diagnostics'},
|
|
456
419
|
{id: 'bootup-time', weight: 0, group: 'diagnostics'},
|
|
457
420
|
{id: 'mainthread-work-breakdown', weight: 0, group: 'diagnostics'},
|
|
458
|
-
{id: 'font-display', weight: 0, group: 'diagnostics'},
|
|
459
|
-
{id: 'third-party-summary', weight: 0, group: 'diagnostics'},
|
|
460
|
-
{id: 'largest-contentful-paint-element', weight: 0, group: 'diagnostics'},
|
|
461
|
-
{id: 'lcp-lazy-loaded', weight: 0, group: 'diagnostics'},
|
|
462
|
-
{id: 'layout-shifts', weight: 0, group: 'diagnostics'},
|
|
463
421
|
{id: 'uses-passive-event-listeners', weight: 0, group: 'diagnostics'},
|
|
464
422
|
{id: 'no-document-write', weight: 0, group: 'diagnostics'},
|
|
465
423
|
{id: 'long-tasks', weight: 0, group: 'diagnostics'},
|
|
466
424
|
{id: 'non-composited-animations', weight: 0, group: 'diagnostics'},
|
|
467
425
|
{id: 'unsized-images', weight: 0, group: 'diagnostics'},
|
|
468
|
-
{id: 'viewport', weight: 0, group: 'diagnostics'},
|
|
469
|
-
{id: 'uses-responsive-images-snapshot', weight: 0, group: 'diagnostics'},
|
|
470
|
-
{id: 'work-during-interaction', weight: 0, group: 'diagnostics'},
|
|
471
426
|
{id: 'bf-cache', weight: 0, group: 'diagnostics'},
|
|
472
427
|
|
|
473
428
|
// Audits past this point contain useful data but are not displayed with other audits.
|
|
@@ -481,6 +436,9 @@ const defaultConfig = {
|
|
|
481
436
|
{id: 'final-screenshot', weight: 0, group: 'hidden'},
|
|
482
437
|
{id: 'script-treemap-data', weight: 0, group: 'hidden'},
|
|
483
438
|
{id: 'resource-summary', weight: 0, group: 'hidden'},
|
|
439
|
+
{id: 'redirects', weight: 0, group: 'hidden'},
|
|
440
|
+
{id: 'server-response-time', weight: 0, group: 'hidden'},
|
|
441
|
+
{id: 'layout-shifts', weight: 0, group: 'hidden'},
|
|
484
442
|
],
|
|
485
443
|
},
|
|
486
444
|
'accessibility': {
|
|
@@ -610,7 +568,6 @@ const defaultConfig = {
|
|
|
610
568
|
{id: 'paste-preventing-inputs', weight: 3, group: 'best-practices-ux'},
|
|
611
569
|
{id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'},
|
|
612
570
|
{id: 'image-size-responsive', weight: 1, group: 'best-practices-ux'},
|
|
613
|
-
{id: 'viewport', weight: 1, group: 'best-practices-ux'},
|
|
614
571
|
// Browser Compatibility
|
|
615
572
|
{id: 'doctype', weight: 1, group: 'best-practices-browser-compat'},
|
|
616
573
|
{id: 'charset', weight: 1, group: 'best-practices-browser-compat'},
|