@salesforce/storefront-next-dev 1.4.0 → 1.5.0-alpha.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/README.md +13 -2
- package/dist/assets.js +25 -0
- package/dist/audit/catalog/categories/best-practices/CATEGORY.md +21 -0
- package/dist/audit/catalog/categories/best-practices/rules/commerce-api-client.md +20 -0
- package/dist/audit/catalog/categories/best-practices/rules/deferred-ui.js +575 -0
- package/dist/audit/catalog/categories/best-practices/rules/deferred-ui.md +20 -0
- package/dist/audit/catalog/categories/best-practices/rules/error-boundaries.md +19 -0
- package/dist/audit/catalog/categories/best-practices/rules/production-integrations.md +20 -0
- package/dist/audit/catalog/categories/best-practices/rules/route-revalidation.md +19 -0
- package/dist/audit/catalog/categories/best-practices/rules/server-data-loading.md +19 -0
- package/dist/audit/catalog/categories/performance/CATEGORY.md +18 -0
- package/dist/audit/catalog/categories/performance/rules/app-config-size.js +15 -0
- package/dist/audit/catalog/categories/performance/rules/app-config-size.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/cache-control.js +59 -0
- package/dist/audit/catalog/categories/performance/rules/cache-control.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/cls-budget.js +13 -0
- package/dist/audit/catalog/categories/performance/rules/cls-budget.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/content-encoding.js +35 -0
- package/dist/audit/catalog/categories/performance/rules/content-encoding.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/dom-size.js +18 -0
- package/dist/audit/catalog/categories/performance/rules/dom-size.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/font-display.js +248 -0
- package/dist/audit/catalog/categories/performance/rules/font-display.md +21 -0
- package/dist/audit/catalog/categories/performance/rules/html-document-size.js +15 -0
- package/dist/audit/catalog/categories/performance/rules/html-document-size.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/image-dimensions.js +141 -0
- package/dist/audit/catalog/categories/performance/rules/image-dimensions.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/image-transformation.js +117 -0
- package/dist/audit/catalog/categories/performance/rules/image-transformation.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/javascript-size.js +126 -0
- package/dist/audit/catalog/categories/performance/rules/javascript-size.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/large-images.js +187 -0
- package/dist/audit/catalog/categories/performance/rules/large-images.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-budget.js +13 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-budget.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.js +227 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.md +27 -0
- package/dist/audit/catalog/categories/performance/rules/page-data-size.js +15 -0
- package/dist/audit/catalog/categories/performance/rules/page-data-size.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.js +97 -0
- package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.md +73 -0
- package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.js +201 -0
- package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/server-request-waterfall.md +85 -0
- package/dist/audit/catalog/categories/performance/rules/tbt-budget.js +13 -0
- package/dist/audit/catalog/categories/performance/rules/tbt-budget.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/third-party-fonts.js +265 -0
- package/dist/audit/catalog/categories/performance/rules/third-party-fonts.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/ttfb-budget.js +17 -0
- package/dist/audit/catalog/categories/performance/rules/ttfb-budget.md +20 -0
- package/dist/audit/catalog/categories/security/CATEGORY.md +17 -0
- package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.js +37 -0
- package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.md +20 -0
- package/dist/audit/catalog/categories/security/rules/content-security-policy.js +40 -0
- package/dist/audit/catalog/categories/security/rules/content-security-policy.md +20 -0
- package/dist/audit/catalog/categories/security/rules/csp-runtime-compatibility.md +24 -0
- package/dist/audit/catalog/categories/security/rules/dependency-vulnerabilities.md +19 -0
- package/dist/audit/catalog/categories/security/rules/dynamic-code-execution.md +19 -0
- package/dist/audit/catalog/categories/security/rules/hardcoded-credentials.md +19 -0
- package/dist/audit/catalog/categories/security/rules/html-config-secrets.js +46 -0
- package/dist/audit/catalog/categories/security/rules/html-config-secrets.md +22 -0
- package/dist/audit/catalog/categories/security/rules/protected-route-authorization.md +19 -0
- package/dist/audit/catalog/categories/security/rules/public-client-secrets.md +20 -0
- package/dist/audit/catalog/categories/security/rules/raw-html-sinks.md +20 -0
- package/dist/audit/catalog/categories/security/rules/strict-transport-security.js +183 -0
- package/dist/audit/catalog/categories/security/rules/strict-transport-security.md +24 -0
- package/dist/audit/catalog/categories/seo/CATEGORY.md +17 -0
- package/dist/audit/catalog/categories/seo/rules/canonical-url.md +20 -0
- package/dist/audit/catalog/categories/seo/rules/locale-alternates.md +19 -0
- package/dist/audit/catalog/categories/seo/rules/meta-description.md +19 -0
- package/dist/audit/catalog/categories/seo/rules/page-title.md +19 -0
- package/dist/audit/catalog/categories/seo/rules/robots-indexing.md +20 -0
- package/dist/audit/catalog/categories/seo/rules/structured-data.md +19 -0
- package/dist/audit/instructions/evaluate-rule.md +42 -0
- package/dist/audit/schemas/audit-result.schema.json +1237 -0
- package/dist/audit/ui/assets/banner-brand-default-CDRB3_S0.png +0 -0
- package/dist/audit/ui/assets/banner-group-public-default-BGRJzVY8.png +0 -0
- package/dist/audit/ui/assets/banner-user-default-BW81Ba3z.png +0 -0
- package/dist/audit/ui/assets/bg-info@2x-CzpP0vaE.png +0 -0
- package/dist/audit/ui/assets/einstein-figure-B1xFOo8O.svg +1 -0
- package/dist/audit/ui/assets/group_avatar_160-CrxVHlYA.png +0 -0
- package/dist/audit/ui/assets/group_avatar_200-CSLS8I0r.png +0 -0
- package/dist/audit/ui/assets/index-CtE_JUcs.css +1 -0
- package/dist/audit/ui/assets/index-aSdbHIeq.js +44 -0
- package/dist/audit/ui/assets/profile_avatar_160-CtQTUBo0.png +0 -0
- package/dist/audit/ui/assets/profile_avatar_200-DAztxshF.png +0 -0
- package/dist/audit/ui/assets/symbols-BRmpfDbO.svg +1 -0
- package/dist/audit/ui/assets/symbols-D0Ne6690.svg +1 -0
- package/dist/audit/ui/einstein.png +0 -0
- package/dist/audit/ui/index.html +13 -0
- package/dist/audit.js +1898 -0
- package/dist/cartridge-services/index.d.ts +5 -0
- package/dist/cartridge-services/index.d.ts.map +1 -1
- package/dist/cartridge-services/index.js +94 -7
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/catalog.js +89 -0
- package/dist/commands/audit/catalog.js +27 -0
- package/dist/commands/audit/finalize.js +42 -0
- package/dist/commands/audit/prepare.js +84 -0
- package/dist/commands/audit/progress.js +51 -0
- package/dist/commands/audit/serve.js +63 -0
- package/dist/commands/config/aggregate-extensions.js +1 -0
- package/dist/commands/create-bundle.js +2 -1
- package/dist/commands/create-instructions.js +1 -0
- package/dist/commands/create-storefront.js +1 -0
- package/dist/commands/deploy-cartridge.js +35 -11
- package/dist/commands/dev.js +2 -0
- package/dist/commands/extensions/create.js +1 -0
- package/dist/commands/extensions/install.js +1 -0
- package/dist/commands/extensions/list.js +1 -0
- package/dist/commands/extensions/remove.js +1 -0
- package/dist/commands/generate-cartridge.js +49 -5
- package/dist/commands/locales/aggregate-extensions.js +1 -0
- package/dist/commands/prepare-local.js +1 -0
- package/dist/commands/preview.js +2 -0
- package/dist/commands/push.js +37 -7
- package/dist/commands/validate-cartridge.js +13 -4
- package/dist/config.js +13 -2
- package/dist/document-size.js +231 -0
- package/dist/events.js +688 -0
- package/dist/flags2.js +34 -0
- package/dist/generate-cartridge.js +41 -6
- package/dist/har.js +205 -0
- package/dist/images.js +146 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/live.js +277 -0
- package/dist/logger.js +1 -4
- package/dist/measured-run.js +91 -0
- package/dist/metric-budget.js +125 -0
- package/dist/mrt/ssr.mjs +50 -50
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +56 -56
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/package.js +5 -0
- package/dist/sanitize.js +185 -0
- package/dist/schema.js +121 -0
- package/dist/server.js +8 -60
- package/dist/server2.js +185 -0
- package/dist/ts-import.js +57 -0
- package/package.json +24 -6
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: best-practices/error-boundaries
|
|
3
|
+
title: Route Error Pages
|
|
4
|
+
severity: medium
|
|
5
|
+
appliesTo: [route]
|
|
6
|
+
inputs: [source.code, manual.failure-state]
|
|
7
|
+
---
|
|
8
|
+
## Intent
|
|
9
|
+
Checks that when a route's loader, action or rendering fails, shoppers see a clear error page with the right HTTP status and a way to continue, and that the error is logged. Without this, a failure shows a blank or misleading page.
|
|
10
|
+
## Inspect
|
|
11
|
+
Trace representative route failures to their route-level error handling and inspect the user-visible status, recovery action, and logging boundary.
|
|
12
|
+
## Evaluate
|
|
13
|
+
Report missing or misleading handling for a demonstrated production failure path; do not require a particular framework abstraction by name.
|
|
14
|
+
## Evidence
|
|
15
|
+
Cite the failure path, route source, response/status behavior, and visible fallback.
|
|
16
|
+
## Remediation
|
|
17
|
+
Add scoped error handling that preserves status semantics, safe messaging, recovery, and server-side diagnostics.
|
|
18
|
+
## Examples
|
|
19
|
+
Compliant: failed product load renders a scoped retry state with correct status. Problematic: uncaught failure renders blank content. Ambiguous: global platform handling not observable.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: best-practices/production-integrations
|
|
3
|
+
title: No Mock or Demo Integrations
|
|
4
|
+
severity: critical
|
|
5
|
+
appliesTo: [cart, checkout, account]
|
|
6
|
+
inputs: [source.code, manual.end-to-end]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Intent
|
|
10
|
+
Checks that cart, checkout, payment, order management, tax and loyalty flows call real systems, not mock, demo or TODO code left over from development. A stubbed integration in these flows can lose orders or revenue at launch.
|
|
11
|
+
## Inspect
|
|
12
|
+
Trace cart, checkout, payment, OMS, tax, and loyalty flows for demo/mock/TODO paths and validate opaque systems end to end.
|
|
13
|
+
## Evaluate
|
|
14
|
+
Use finding for demonstrated stub behavior; use `not-verified` for opaque integrations lacking test evidence.
|
|
15
|
+
## Evidence
|
|
16
|
+
Cite executable source path or end-to-end result, never a comment marker alone.
|
|
17
|
+
## Remediation
|
|
18
|
+
Wire the production integration or disable the feature until verified.
|
|
19
|
+
## Examples
|
|
20
|
+
Compliant: real integration plus successful E2E evidence. Problematic: mock success on place-order. Ambiguous: TODO comment beside complete code.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: best-practices/route-revalidation
|
|
3
|
+
title: "Layout Loaders Don't Rerun Needlessly"
|
|
4
|
+
severity: medium
|
|
5
|
+
appliesTo: [home, plp, pdp]
|
|
6
|
+
inputs: [source.code]
|
|
7
|
+
---
|
|
8
|
+
## Intent
|
|
9
|
+
Checks that loaders for layouts that stay on screen across pages, such as the app shell, use `shouldRevalidate` so they don't fetch the same data again on every navigation. Rerunning an expensive layout loader slows down every page change.
|
|
10
|
+
## Inspect
|
|
11
|
+
Identify persistent layout loaders and inspect shouldRevalidate against their actual inputs.
|
|
12
|
+
## Evaluate
|
|
13
|
+
Finding requires repeated expensive work without a valid revalidation need.
|
|
14
|
+
## Evidence
|
|
15
|
+
Cite loader, mounted scope, calls, and revalidation behavior.
|
|
16
|
+
## Remediation
|
|
17
|
+
Gate revalidation to meaningful input changes.
|
|
18
|
+
## Examples
|
|
19
|
+
Compliant: scoped shouldRevalidate. Problematic: shell fan-out each navigation. Ambiguous: cheap loader.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: best-practices/server-data-loading
|
|
3
|
+
title: First-Render Data Loads on the Server
|
|
4
|
+
severity: high
|
|
5
|
+
appliesTo: [home, plp, pdp]
|
|
6
|
+
inputs: [source.code]
|
|
7
|
+
---
|
|
8
|
+
## Intent
|
|
9
|
+
Checks that the data a page needs for its first render is loaded in server loaders, not fetched in the browser after hydration (with `clientLoader` or a `useEffect` fetch). Loading it on the server sends shoppers and search engines complete HTML without waiting for JavaScript.
|
|
10
|
+
## Inspect
|
|
11
|
+
Trace clientLoader, clientAction, useEffect fetches, and route loader ownership.
|
|
12
|
+
## Evaluate
|
|
13
|
+
Finding requires first-render data being unnecessarily delayed until hydration.
|
|
14
|
+
## Evidence
|
|
15
|
+
Cite route, data need, client call, and available server path.
|
|
16
|
+
## Remediation
|
|
17
|
+
Move required initial data to a server loader/action.
|
|
18
|
+
## Examples
|
|
19
|
+
Compliant: loader-fed render. Problematic: PDP fetch starts after hydration. Ambiguous: post-interaction effect fetch.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Performance
|
|
2
|
+
|
|
3
|
+
## Purpose and boundaries
|
|
4
|
+
|
|
5
|
+
Use Playhouse summary, Opportunities, LCP, TBT, third-party, document, and network artifacts as measured evidence. Inspect source only to explain measured symptoms or delivered response policy; never recreate Playhouse algorithms. The exception is `performance/server-request-waterfall`, which maps server-render network calls from source because Playhouse can't see inside the server.
|
|
6
|
+
|
|
7
|
+
## Investigation sequence
|
|
8
|
+
|
|
9
|
+
Inspect every rule, compare representative and outlier cells, then trace measured symptoms to source.
|
|
10
|
+
|
|
11
|
+
## Evidence and redaction
|
|
12
|
+
|
|
13
|
+
Cite artifact name, JSON pointer/value, page, and line-addressable source where used.
|
|
14
|
+
|
|
15
|
+
## Severity and blind spots
|
|
16
|
+
|
|
17
|
+
Uncollected and authenticated pages remain explicit coverage gaps. Header policy requires a representative sanitized HAR with trustworthy response type and size evidence.
|
|
18
|
+
Public-resource caching does not establish that personalized or API responses are safe for shared caches.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "../../../../../har.js";
|
|
2
|
+
import { i as kib, n as documentSizeDataSchema, r as documentSizeRule } from "../../../../../document-size.js";
|
|
3
|
+
|
|
4
|
+
//#region src/audit/catalog/categories/performance/rules/app-config-size.ts
|
|
5
|
+
var app_config_size_default = documentSizeRule({
|
|
6
|
+
label: "App config",
|
|
7
|
+
budget: 12 * 1024,
|
|
8
|
+
value: (size) => size.appConfigBytes,
|
|
9
|
+
format: kib,
|
|
10
|
+
evidence: (size) => size.scripts.appConfig[0] ? { artifact: size.scripts.appConfig[0] } : null,
|
|
11
|
+
decideFailures: false
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { documentSizeDataSchema as dataSchema, app_config_size_default as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: performance/app-config-size
|
|
3
|
+
title: App Config Size
|
|
4
|
+
severity: low
|
|
5
|
+
appliesTo: [home, plp, pdp]
|
|
6
|
+
inputs: [playhouse.scripts, source.code]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Intent
|
|
10
|
+
Checks that the app config Storefront Next writes into every page (`window.__APP_CONFIG__`) is 12 KiB or less. The config is repeated in each page's HTML, so settings the browser never reads add to every page load. When a page is over budget, the report points to the config sections the browser doesn't need.
|
|
11
|
+
## Inspect
|
|
12
|
+
The rule script measures the archived `window.__APP_CONFIG__` script in each page's HTML. If every page is within 12 KiB, it passes and this rule needs no analysis.
|
|
13
|
+
|
|
14
|
+
Otherwise, use the `data` file (sizes are in bytes): `appConfigSections` gives each top-level config section's size, largest first, and `scripts.appConfig` names the archived config script (relative to the page's `archiveDir`). Read the largest sections in that script, then check in the project where each large section is read (`getConfig`, `useConfig`, `window.__APP_CONFIG__`) and whether the browser needs it, or only server code does. Settings browser code reads at any point, including after hydration or on interaction, are needed. Also check whether the root loader returns the same config, which embeds it in the page data a second time.
|
|
15
|
+
## Evaluate
|
|
16
|
+
Report one `finding` for the rule when any page's config is over 12 KiB; when the config is the same on every page, cite it once and say so. Name the largest sections and, for each, whether the browser reads it, with an approximate size. Suggest keeping server-only settings out of the config sent to the browser, as suggestions the customer can choose from.
|
|
17
|
+
## Evidence
|
|
18
|
+
The archived config script (`playhouse`, with its size as `value`) and `source` evidence for where the large sections are defined and read.
|
|
19
|
+
## Remediation
|
|
20
|
+
Leave settings the browser never reads out of the config sent to the browser. Re-run Playhouse to confirm the config is at or under 12 KiB.
|
|
21
|
+
## Examples
|
|
22
|
+
Compliant: a 10 KiB config. Problematic: a 15 KiB config whose largest section is only read on the server.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { a as harRuleDataSchema, d as sameHostResponses, i as harRule, o as header, r as cacheControl, u as resourceKind } from "../../../../../har.js";
|
|
2
|
+
|
|
3
|
+
//#region src/audit/catalog/categories/performance/rules/cache-control.ts
|
|
4
|
+
const dataSchema = harRuleDataSchema;
|
|
5
|
+
const YEAR = 31536e3;
|
|
6
|
+
const HOUR = 3600;
|
|
7
|
+
const DAY = 86400;
|
|
8
|
+
/** A content hash before the extension, e.g. `entry.client-Be9z2OkY.js` or `main.3f2a9c0d.css`; it needs a digit or an uppercase letter so words don't count. */
|
|
9
|
+
function fingerprinted(url) {
|
|
10
|
+
const hash = /[.-]([A-Za-z0-9_-]{8}|[a-f0-9]{8,})\.(?:m?js|css)$/.exec(new URL(url).pathname)?.[1];
|
|
11
|
+
return hash !== void 0 && /[0-9A-Z]/.test(hash);
|
|
12
|
+
}
|
|
13
|
+
function minimumMaxAge(entry) {
|
|
14
|
+
const kind = resourceKind(entry);
|
|
15
|
+
if (kind === "js" || kind === "css") return fingerprinted(entry.request.url) ? {
|
|
16
|
+
seconds: YEAR,
|
|
17
|
+
label: "a fingerprinted asset"
|
|
18
|
+
} : {
|
|
19
|
+
seconds: HOUR,
|
|
20
|
+
label: "a non-fingerprinted script or stylesheet"
|
|
21
|
+
};
|
|
22
|
+
if (kind === "image" || kind === "font") return {
|
|
23
|
+
seconds: DAY,
|
|
24
|
+
label: `a public ${kind}`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
var cache_control_default = harRule("same-host script, stylesheet, image and font responses", (match) => {
|
|
28
|
+
const eligible = sameHostResponses(match).flatMap((item) => {
|
|
29
|
+
const minimum = minimumMaxAge(item.entry);
|
|
30
|
+
return minimum ? [{
|
|
31
|
+
item,
|
|
32
|
+
minimum
|
|
33
|
+
}] : [];
|
|
34
|
+
});
|
|
35
|
+
const failures = eligible.flatMap(({ item, minimum }) => {
|
|
36
|
+
const value = header(item.entry, "cache-control");
|
|
37
|
+
const directives = cacheControl(item.entry);
|
|
38
|
+
const reasons = [];
|
|
39
|
+
if (!value) reasons.push("Cache-Control is missing.");
|
|
40
|
+
else {
|
|
41
|
+
for (const directive of ["private", "no-store"]) if (directives.has(directive)) reasons.push(`Cache-Control is ${directive}.`);
|
|
42
|
+
const maxAge = directives.get("max-age");
|
|
43
|
+
if (maxAge === void 0 || !/^\d+$/.test(maxAge)) reasons.push("Cache-Control has no valid max-age.");
|
|
44
|
+
else if (Number(maxAge) < minimum.seconds) reasons.push(`max-age=${maxAge} is under ${minimum.seconds} seconds for ${minimum.label}.`);
|
|
45
|
+
}
|
|
46
|
+
return reasons.length ? [{
|
|
47
|
+
entry: item,
|
|
48
|
+
reasons,
|
|
49
|
+
headers: { "cache-control": value }
|
|
50
|
+
}] : [];
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
responses: eligible.length,
|
|
54
|
+
failures
|
|
55
|
+
};
|
|
56
|
+
}, "have long enough Cache-Control lifetimes");
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { dataSchema, cache_control_default as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: performance/cache-control
|
|
3
|
+
title: Browser Cache Lifetimes for Static Files
|
|
4
|
+
severity: medium
|
|
5
|
+
appliesTo: [home, plp, pdp]
|
|
6
|
+
inputs: [playhouse.network-har]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Intent
|
|
10
|
+
Checks the `Cache-Control` header on the storefront's own scripts, stylesheets, images and fonts: at least 1 year for fingerprinted scripts and stylesheets (a content hash in the file name, e.g. `entry.client-Be9z2OkY.js`), 1 hour for other scripts and stylesheets, and 1 day for images and fonts. Long enough lifetimes let browsers and CDNs reuse files instead of downloading them again. HTML, API responses and third-party files are not checked.
|
|
11
|
+
## Inspect
|
|
12
|
+
The rule script matches the final document response in `network-mobile-cold.har` (via `network-mobile-cold.json`) for each page. Only same-host responses with status 200 are checked; third-party responses are skipped. It classifies scripts, stylesheets, images and fonts by content type, treats a script or stylesheet as fingerprinted when its file name carries a content hash (e.g. `entry.client-Be9z2OkY.js`), and reads `Cache-Control`. If no page's document can be matched, the rule is not decided.
|
|
13
|
+
## Evaluate
|
|
14
|
+
Fail when `Cache-Control` is missing, `private` or `no-store`, or its `max-age` is missing or under: 1 year for fingerprinted scripts and stylesheets, 1 hour for other scripts and stylesheets, 1 day for images and fonts. HTML and API/JSON responses are not graded. Pass otherwise.
|
|
15
|
+
## Evidence
|
|
16
|
+
The HAR entry of each failing response (up to 10), with its `Cache-Control`.
|
|
17
|
+
## Remediation
|
|
18
|
+
Set the policy at the layer that owns the response: Managed Runtime or the public edge for bundle assets, a customer CDN when it rewrites headers, application code for application-owned responses. Use `public, max-age=31536000, immutable` only for content-hashed files and shorter lifetimes for URLs that change in place. Re-run Playhouse to confirm.
|
|
19
|
+
## Examples
|
|
20
|
+
Compliant: `app-a1b2c3d4.js` with `public, max-age=31536000`. Problematic: a fingerprinted bundle with no `Cache-Control`, or a product image with `private, no-store`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import "../../../../../measured-run.js";
|
|
2
|
+
import { n as metricBudget, t as dataSchema } from "../../../../../metric-budget.js";
|
|
3
|
+
|
|
4
|
+
//#region src/audit/catalog/categories/performance/rules/cls-budget.ts
|
|
5
|
+
var cls_budget_default = metricBudget({
|
|
6
|
+
metric: "cls",
|
|
7
|
+
label: "CLS",
|
|
8
|
+
threshold: .1,
|
|
9
|
+
format: (value) => value.toFixed(2)
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { dataSchema, cls_budget_default as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: performance/cls-budget
|
|
3
|
+
title: Cumulative Layout Shift (CLS)
|
|
4
|
+
severity: high
|
|
5
|
+
appliesTo: [home, plp, pdp]
|
|
6
|
+
inputs: [playhouse.summary]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Intent
|
|
10
|
+
Checks that each page's median Cumulative Layout Shift, measured by Lighthouse on mobile with a cold cache, is 0.1 or less, the Core Web Vitals "good" threshold. A high CLS means content jumps around while the page loads.
|
|
11
|
+
## Inspect
|
|
12
|
+
The rule script reads each page's `summary.json` and takes the mobile cold-cache cell's `medianRun.cls`. Playhouse writes 0 for metrics it could not measure, and a rendered page always has a positive LCP, so a run with an LCP of 0 or less measured nothing. Such runs are left out: when any run failed, the script picks the median again from the measured runs the way Playhouse does (the run closest to the median LCP, ties to the earlier run) and reads that run's `runs[].row.cls`.
|
|
13
|
+
## Evaluate
|
|
14
|
+
Pass when every measured page's median CLS is at or under 0.1; fail otherwise. A page without a mobile cold-cache cell, or without any measured run, is skipped. If no page can be measured, the rule is not decided.
|
|
15
|
+
## Evidence
|
|
16
|
+
One `summary.json` pointer per page, `/cells/<i>/medianRun/cls` (or `/runs/<j>/row/cls` when runs were left out), with the measured value and how many runs were not measured.
|
|
17
|
+
## Remediation
|
|
18
|
+
Use `opportunities.json` and `report-mobile-cold.html` for the failing page to find the shifting elements. Reserve space for images, embeds and late-loading content (explicit width/height or aspect-ratio), and avoid inserting content above existing content. Re-run the same Playhouse capture to confirm the mobile cold-cache median is at or under 0.1.
|
|
19
|
+
## Examples
|
|
20
|
+
Compliant: home median CLS 0.04. Problematic: pdp median CLS 0.22.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { a as harRuleDataSchema, d as sameHostResponses, f as tokens, i as harRule, o as header, t as COMPRESSED_ENCODINGS, u as resourceKind } from "../../../../../har.js";
|
|
2
|
+
|
|
3
|
+
//#region src/audit/catalog/categories/performance/rules/content-encoding.ts
|
|
4
|
+
const dataSchema = harRuleDataSchema;
|
|
5
|
+
const MIN_BYTES = {
|
|
6
|
+
html: 1024,
|
|
7
|
+
js: 10 * 1024,
|
|
8
|
+
css: 10 * 1024
|
|
9
|
+
};
|
|
10
|
+
var content_encoding_default = harRule("same-host HTML (1 KiB+), JavaScript and CSS (10 KiB+) responses", (match) => {
|
|
11
|
+
const eligible = sameHostResponses(match).filter(({ entry }) => {
|
|
12
|
+
const min = MIN_BYTES[resourceKind(entry)];
|
|
13
|
+
const size = entry.response.content?.size;
|
|
14
|
+
return min !== void 0 && typeof size === "number" && size >= min;
|
|
15
|
+
});
|
|
16
|
+
const failures = eligible.flatMap((item) => {
|
|
17
|
+
const encoding = header(item.entry, "content-encoding");
|
|
18
|
+
if (tokens(encoding).some((token) => COMPRESSED_ENCODINGS.includes(token))) return [];
|
|
19
|
+
return [{
|
|
20
|
+
entry: item,
|
|
21
|
+
reasons: [`${resourceKind(item.entry).toUpperCase()} response of ${Math.round((item.entry.response.content?.size ?? 0) / 1024)} KiB has no br, gzip or zstd Content-Encoding.`],
|
|
22
|
+
headers: {
|
|
23
|
+
"content-type": header(item.entry, "content-type"),
|
|
24
|
+
"content-encoding": encoding
|
|
25
|
+
}
|
|
26
|
+
}];
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
responses: eligible.length,
|
|
30
|
+
failures
|
|
31
|
+
};
|
|
32
|
+
}, "are compressed with Brotli, gzip or zstd");
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { dataSchema, content_encoding_default as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: performance/content-encoding
|
|
3
|
+
title: HTML, JavaScript and CSS Compression
|
|
4
|
+
severity: high
|
|
5
|
+
appliesTo: [home, plp, pdp]
|
|
6
|
+
inputs: [playhouse.network-har]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Intent
|
|
10
|
+
Checks that the storefront's own HTML (1 KiB or more), JavaScript and CSS (10 KiB or more) are sent compressed with Brotli, gzip or zstd, so pages download fewer bytes. Third-party files and smaller responses are not checked. Managed Runtime compresses by default, so a failure usually points to a CDN or proxy in front of it.
|
|
11
|
+
## Inspect
|
|
12
|
+
The rule script matches the final document response in `network-mobile-cold.har` (via `network-mobile-cold.json`) for each page. Only same-host responses with status 200 are checked; third-party responses are skipped. It reads the content type and decoded size (`response.content`) and `Content-Encoding`. If no page's document can be matched, the rule is not decided.
|
|
13
|
+
## Evaluate
|
|
14
|
+
Fail when HTML of at least 1 KiB, or JavaScript or CSS of at least 10 KiB (decoded size), has no `br`, `gzip` or `zstd` `Content-Encoding`. Responses without a known type or size are skipped. Pass otherwise.
|
|
15
|
+
## Evidence
|
|
16
|
+
The HAR entry of each failing response (up to 10), with its size, `Content-Type` and `Content-Encoding`.
|
|
17
|
+
## Remediation
|
|
18
|
+
Check that the public edge and every customer CDN in front of Managed Runtime preserve its compression; do not add application-level compression on Managed Runtime. Escalate an MRT-owned miss to Salesforce. For a non-MRT origin, enable Brotli or gzip. Re-run Playhouse to confirm.
|
|
19
|
+
## Examples
|
|
20
|
+
Compliant: a 180 KiB bundle with `Content-Encoding: br`. Problematic: a 40 KiB stylesheet with no `Content-Encoding`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "../../../../../har.js";
|
|
2
|
+
import { n as documentSizeDataSchema, r as documentSizeRule, t as count } from "../../../../../document-size.js";
|
|
3
|
+
|
|
4
|
+
//#region src/audit/catalog/categories/performance/rules/dom-size.ts
|
|
5
|
+
var dom_size_default = documentSizeRule({
|
|
6
|
+
label: "Element count",
|
|
7
|
+
budget: 1400,
|
|
8
|
+
value: (size) => size.elements,
|
|
9
|
+
format: count,
|
|
10
|
+
evidence: (size) => size.lighthouseArtifact ? {
|
|
11
|
+
artifact: size.lighthouseArtifact,
|
|
12
|
+
pointer: "/audits/dom-size-insight/details/items"
|
|
13
|
+
} : null,
|
|
14
|
+
decideFailures: false
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { documentSizeDataSchema as dataSchema, dom_size_default as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: performance/dom-size
|
|
3
|
+
title: DOM Size (Number of Elements)
|
|
4
|
+
severity: medium
|
|
5
|
+
appliesTo: [home, plp, pdp]
|
|
6
|
+
inputs: [playhouse.lighthouse, playhouse.scripts, source.code]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Intent
|
|
10
|
+
Checks that each page has 1,400 HTML elements or fewer after it loads, the limit Lighthouse used to flag. Every element adds to the HTML download and to the browser's style, layout and hydration work, so pages with large DOMs render and respond more slowly. When a page is over the limit, the report points to the components that render markup the shopper doesn't need yet.
|
|
11
|
+
## Inspect
|
|
12
|
+
The rule script reads the element count and DOM depth from Lighthouse's DOM size insight (`dom-size-insight`) in each page's mobile cold-cache Lighthouse result. If every page has 1,400 elements or fewer, it passes and this rule needs no analysis.
|
|
13
|
+
|
|
14
|
+
Otherwise, use the `data` file and the Lighthouse result it names (`lighthouseArtifact`). The insight also names the deepest element and the element with the most children. Fetch the page (`curl -sL <url>`) to see which parts of the markup repeat most, then find the components in the project that render them.
|
|
15
|
+
## Evaluate
|
|
16
|
+
Report a `finding` for each page over 1,400 elements. Name the parts of the page that add the most elements and the components behind them. Typical causes: a mega menu that renders every category up front, every carousel slide or variant panel rendered while hidden, long lists without pagination, and deeply nested wrapper elements. Suggest rendering hidden or below-the-fold markup only when it's needed, as suggestions the customer can choose from.
|
|
17
|
+
## Evidence
|
|
18
|
+
The Lighthouse DOM size insight for each page over the limit (`playhouse`, with the element count as `value`), and `source` evidence for each component that renders the most markup.
|
|
19
|
+
## Remediation
|
|
20
|
+
Render menus, hidden panels and off-screen slides when they open, paginate long lists, and remove wrapper elements that add no styling. Re-run Playhouse to confirm each page has 1,400 elements or fewer.
|
|
21
|
+
## Examples
|
|
22
|
+
Compliant: a PLP with 720 elements. Problematic: a PLP with 1,620 elements because every product tile renders all of its color swatches.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { l as normalizedUrl } from "../../../../../har.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/audit/catalog/categories/performance/rules/font-display.ts
|
|
5
|
+
const FORM_FACTORS = ["mobile", "desktop"];
|
|
6
|
+
const SAFE_RUN_ID = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
7
|
+
const TRACE_MAX_BYTES = 256 * 1024 * 1024;
|
|
8
|
+
const acceptable = new Set(["swap", "optional"]);
|
|
9
|
+
const summarySchema = z.object({ runs: z.array(z.object({
|
|
10
|
+
runId: z.string().optional(),
|
|
11
|
+
formFactor: z.string().optional(),
|
|
12
|
+
cacheState: z.string().optional(),
|
|
13
|
+
isMedian: z.boolean().optional()
|
|
14
|
+
}).passthrough()).optional().default([]) }).passthrough();
|
|
15
|
+
const harSchema = z.object({ log: z.object({ entries: z.array(z.object({
|
|
16
|
+
_resourceType: z.string().optional(),
|
|
17
|
+
request: z.object({ url: z.string() }).passthrough(),
|
|
18
|
+
response: z.object({ status: z.number() }).passthrough()
|
|
19
|
+
}).passthrough()) }).passthrough() }).passthrough();
|
|
20
|
+
const fontSchema = z.object({
|
|
21
|
+
url: z.string(),
|
|
22
|
+
display: z.string(),
|
|
23
|
+
status: z.enum(["pass", "fail"]),
|
|
24
|
+
event: z.number()
|
|
25
|
+
}).strict();
|
|
26
|
+
const unverifiedFontSchema = z.object({
|
|
27
|
+
url: z.string(),
|
|
28
|
+
entry: z.number()
|
|
29
|
+
}).strict();
|
|
30
|
+
const cellSchema = z.object({
|
|
31
|
+
cell: z.enum(["mobile-cold", "desktop-cold"]),
|
|
32
|
+
runId: z.string(),
|
|
33
|
+
status: z.enum([
|
|
34
|
+
"pass",
|
|
35
|
+
"fail",
|
|
36
|
+
"undecided"
|
|
37
|
+
]),
|
|
38
|
+
traceArtifact: z.string(),
|
|
39
|
+
harArtifact: z.string(),
|
|
40
|
+
fonts: z.array(fontSchema),
|
|
41
|
+
unverifiedFonts: z.array(unverifiedFontSchema)
|
|
42
|
+
}).strict();
|
|
43
|
+
const dataSchema = z.object({
|
|
44
|
+
pages: z.array(z.object({
|
|
45
|
+
page: z.string(),
|
|
46
|
+
cells: z.array(cellSchema)
|
|
47
|
+
}).strict()),
|
|
48
|
+
skipped: z.array(z.object({
|
|
49
|
+
page: z.string(),
|
|
50
|
+
cell: z.string(),
|
|
51
|
+
reason: z.string()
|
|
52
|
+
}).strict())
|
|
53
|
+
}).strict();
|
|
54
|
+
function object(value) {
|
|
55
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
56
|
+
}
|
|
57
|
+
function traceFonts(trace) {
|
|
58
|
+
const events = object(trace)?.traceEvents;
|
|
59
|
+
if (!Array.isArray(events)) return void 0;
|
|
60
|
+
const fonts = /* @__PURE__ */ new Map();
|
|
61
|
+
for (const [event, raw] of events.entries()) {
|
|
62
|
+
const item = object(raw);
|
|
63
|
+
if (item?.name !== "BeginRemoteFontLoad") continue;
|
|
64
|
+
const args = object(item.args);
|
|
65
|
+
const url = args?.url;
|
|
66
|
+
const display = args?.display;
|
|
67
|
+
if (typeof url !== "string" || typeof display !== "string") continue;
|
|
68
|
+
const key = normalizedUrl(url) ?? url;
|
|
69
|
+
const policy = display.toLowerCase();
|
|
70
|
+
const candidate = {
|
|
71
|
+
url,
|
|
72
|
+
display: policy,
|
|
73
|
+
status: acceptable.has(policy) ? "pass" : "fail",
|
|
74
|
+
event
|
|
75
|
+
};
|
|
76
|
+
const known = fonts.get(key);
|
|
77
|
+
if (!known || known.status === "pass" && candidate.status === "fail") fonts.set(key, candidate);
|
|
78
|
+
}
|
|
79
|
+
return [...fonts.values()];
|
|
80
|
+
}
|
|
81
|
+
async function medianRun(playhouse, page, formFactor) {
|
|
82
|
+
const runs = ((await playhouse.readJson(page, "summary.json", summarySchema))?.runs ?? []).filter((run$1) => run$1.runId && SAFE_RUN_ID.test(run$1.runId) && run$1.formFactor === formFactor && run$1.cacheState === "cold");
|
|
83
|
+
return (runs.find((run$1) => run$1.isMedian) ?? runs[0])?.runId ?? `${formFactor}-cold-1`;
|
|
84
|
+
}
|
|
85
|
+
async function measureCell(playhouse, page, formFactor) {
|
|
86
|
+
const cell = `${formFactor}-cold`;
|
|
87
|
+
try {
|
|
88
|
+
const runId = await medianRun(playhouse, page, formFactor);
|
|
89
|
+
const traceArtifact = `runs/${runId}/trace.json`;
|
|
90
|
+
const harArtifact = `runs/${runId}/network.har`;
|
|
91
|
+
let traceText;
|
|
92
|
+
let har;
|
|
93
|
+
try {
|
|
94
|
+
[traceText, har] = await Promise.all([playhouse.readText(page, traceArtifact, TRACE_MAX_BYTES), playhouse.readJson(page, harArtifact, harSchema)]);
|
|
95
|
+
} catch {
|
|
96
|
+
return {
|
|
97
|
+
cell,
|
|
98
|
+
reason: `${traceArtifact} could not be read or parsed safely.`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (traceText === void 0) return {
|
|
102
|
+
cell,
|
|
103
|
+
reason: `${traceArtifact} is missing.`
|
|
104
|
+
};
|
|
105
|
+
let trace;
|
|
106
|
+
try {
|
|
107
|
+
trace = JSON.parse(traceText);
|
|
108
|
+
} catch {
|
|
109
|
+
return {
|
|
110
|
+
cell,
|
|
111
|
+
reason: `${traceArtifact} could not be read or parsed safely.`
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const tracePolicies = traceFonts(trace);
|
|
115
|
+
if (!tracePolicies) return {
|
|
116
|
+
cell,
|
|
117
|
+
reason: `${traceArtifact} has no traceEvents array.`
|
|
118
|
+
};
|
|
119
|
+
if (!har) return {
|
|
120
|
+
cell,
|
|
121
|
+
reason: `${harArtifact} is missing.`
|
|
122
|
+
};
|
|
123
|
+
const downloaded = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const [entry, item] of har.log.entries.entries()) {
|
|
125
|
+
if (item._resourceType?.toLowerCase() !== "font" || item.response.status <= 0 || item.response.status >= 400) continue;
|
|
126
|
+
const key = normalizedUrl(item.request.url) ?? item.request.url;
|
|
127
|
+
if (!downloaded.has(key)) downloaded.set(key, {
|
|
128
|
+
url: item.request.url,
|
|
129
|
+
entry
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const fonts = tracePolicies.filter((font) => downloaded.has(normalizedUrl(font.url) ?? font.url));
|
|
133
|
+
const traced = new Set(fonts.map((font) => normalizedUrl(font.url) ?? font.url));
|
|
134
|
+
const unverifiedFonts = [...downloaded].flatMap(([key, item]) => traced.has(key) ? [] : [item]);
|
|
135
|
+
return {
|
|
136
|
+
cell,
|
|
137
|
+
runId,
|
|
138
|
+
status: fonts.some((font) => font.status === "fail") ? "fail" : unverifiedFonts.length ? "undecided" : "pass",
|
|
139
|
+
traceArtifact,
|
|
140
|
+
harArtifact,
|
|
141
|
+
fonts,
|
|
142
|
+
unverifiedFonts
|
|
143
|
+
};
|
|
144
|
+
} catch {
|
|
145
|
+
return {
|
|
146
|
+
cell,
|
|
147
|
+
reason: `${cell} artifacts could not be read or parsed safely.`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function fileName(url) {
|
|
152
|
+
try {
|
|
153
|
+
return decodeURIComponent(new URL(url).pathname.split("/").at(-1) || url);
|
|
154
|
+
} catch {
|
|
155
|
+
return url;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function hostName(url) {
|
|
159
|
+
try {
|
|
160
|
+
return new URL(url).host;
|
|
161
|
+
} catch {
|
|
162
|
+
return url;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const run = async ({ pages, playhouse }) => {
|
|
166
|
+
const data = {
|
|
167
|
+
pages: [],
|
|
168
|
+
skipped: []
|
|
169
|
+
};
|
|
170
|
+
const failures = [];
|
|
171
|
+
const passEvidence = [];
|
|
172
|
+
let undecided = false;
|
|
173
|
+
const pageMeasurements = [];
|
|
174
|
+
for (const page of pages) pageMeasurements.push({
|
|
175
|
+
page,
|
|
176
|
+
measured: await Promise.all(FORM_FACTORS.map((formFactor) => measureCell(playhouse, page.label, formFactor)))
|
|
177
|
+
});
|
|
178
|
+
for (const { page, measured } of pageMeasurements) {
|
|
179
|
+
const cells = [];
|
|
180
|
+
for (const result of measured) {
|
|
181
|
+
if ("reason" in result) {
|
|
182
|
+
data.skipped.push({
|
|
183
|
+
page: page.label,
|
|
184
|
+
cell: result.cell,
|
|
185
|
+
reason: result.reason
|
|
186
|
+
});
|
|
187
|
+
undecided = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
cells.push(result);
|
|
191
|
+
undecided ||= result.status === "undecided";
|
|
192
|
+
for (const font of result.fonts) {
|
|
193
|
+
const evidence = {
|
|
194
|
+
type: "playhouse",
|
|
195
|
+
page: page.label,
|
|
196
|
+
artifact: result.traceArtifact,
|
|
197
|
+
pointer: `/traceEvents/${font.event}/args/display`,
|
|
198
|
+
value: `${fileName(font.url)} from ${hostName(font.url)} uses font-display: ${font.display} in ${result.cell}.`
|
|
199
|
+
};
|
|
200
|
+
if (font.status === "fail") failures.push({
|
|
201
|
+
page: page.label,
|
|
202
|
+
cell: result,
|
|
203
|
+
font
|
|
204
|
+
});
|
|
205
|
+
else passEvidence.push(evidence);
|
|
206
|
+
}
|
|
207
|
+
if (!result.fonts.length) passEvidence.push({
|
|
208
|
+
type: "playhouse",
|
|
209
|
+
page: page.label,
|
|
210
|
+
artifact: result.harArtifact,
|
|
211
|
+
pointer: "/log/entries",
|
|
212
|
+
value: `${result.cell} downloaded no web fonts.`
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
data.pages.push({
|
|
216
|
+
page: page.label,
|
|
217
|
+
cells
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (failures.length) return {
|
|
221
|
+
data,
|
|
222
|
+
decision: {
|
|
223
|
+
status: "fail",
|
|
224
|
+
severity: "medium",
|
|
225
|
+
summary: failures.map(({ page, cell, font }) => `${page} ${cell.cell}: ${fileName(font.url)} from ${hostName(font.url)} uses font-display: ${font.display}.`).join(" "),
|
|
226
|
+
evidence: failures.slice(0, 10).map(({ page, cell, font }) => ({
|
|
227
|
+
type: "playhouse",
|
|
228
|
+
page,
|
|
229
|
+
artifact: cell.traceArtifact,
|
|
230
|
+
pointer: `/traceEvents/${font.event}/args/display`,
|
|
231
|
+
value: `${fileName(font.url)} from ${hostName(font.url)} uses font-display: ${font.display} in ${cell.cell}.`
|
|
232
|
+
}))
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
if (!data.pages.length || undecided || data.skipped.length) return { data };
|
|
236
|
+
return {
|
|
237
|
+
data,
|
|
238
|
+
decision: {
|
|
239
|
+
status: "pass",
|
|
240
|
+
summary: `Every measured web font uses font-display: swap or optional on ${data.pages.map((page) => page.page).join(", ")}.`,
|
|
241
|
+
evidence: passEvidence
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
var font_display_default = run;
|
|
246
|
+
|
|
247
|
+
//#endregion
|
|
248
|
+
export { dataSchema, font_display_default as default };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: performance/font-display
|
|
3
|
+
title: Web Font Display Policy
|
|
4
|
+
severity: medium
|
|
5
|
+
appliesTo: [home, plp, pdp]
|
|
6
|
+
inputs: [playhouse.summary, playhouse.trace, playhouse.network-har, source]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Intent
|
|
10
|
+
Checks that web fonts do not hide text while they download. A blocking `font-display` policy can leave shoppers looking at invisible text, while `swap` and `optional` allow fallback text to render immediately.
|
|
11
|
+
## Inspect
|
|
12
|
+
The rule script reads the median mobile and desktop cold-cache runs from `summary.json`. In each run's trace it finds `BeginRemoteFontLoad` events and reads the font URL and display policy that Chrome used. It compares those URLs with downloaded font responses in the same run's HAR. Chrome does not emit this trace event for every preloaded font, so a downloaded font with no matching event is left undecided for source inspection rather than treated as a pass. For those candidates, inspect the matching `@font-face` declaration in project or fetched stylesheet CSS.
|
|
13
|
+
The layout-shift portion of the original rule specification is deferred until the audit captures a deterministic mapping between layout-shift events and the web font that caused them; this rule currently reports display policy only.
|
|
14
|
+
## Evaluate
|
|
15
|
+
Fail when Chrome reports `auto`, `block`, `fallback`, or any policy other than `swap` or `optional`. Pass only when every downloaded font in both measured cold-cache runs has a trace event and every event uses `swap` or `optional`. When a downloaded font has no matching event, the script leaves the rule undecided; source analysis fails a matching `@font-face` without `swap` or `optional`, passes it when the policy is present, and reports not verified when the declaration cannot be found. A missing trace or HAR also leaves the rule undecided.
|
|
16
|
+
## Evidence
|
|
17
|
+
For a deterministic failure, the trace event's font URL, display policy, form factor and run. For an undecided downloaded font, its HAR URL and the matching `@font-face` declaration when source analysis can find it.
|
|
18
|
+
## Remediation
|
|
19
|
+
Set `font-display: swap` for brand fonts that should replace fallback text as soon as they arrive, or `font-display: optional` when avoiding a late swap is more important than using the web font on the first visit. Add fallback metrics such as `size-adjust`, `ascent-override`, `descent-override` and `line-gap-override` when a font swap contributes to layout shift. Replace icon fonts with inline SVG or an SVG sprite so icons do not depend on a blocking font. Self-host third-party fonts when licensing permits; otherwise preload only critical faces and preconnect to the font origin. Apply the policy to every relevant `@font-face`, including preloaded faces, and re-run both mobile and desktop cold-cache captures.
|
|
20
|
+
## Examples
|
|
21
|
+
Compliant: `@font-face { font-family: Brand; src: url('/fonts/brand.woff2') format('woff2'); font-display: swap; }`. Problematic: a downloaded face whose trace reports `font-display: block`, or a preloaded face whose declaration omits `font-display`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "../../../../../har.js";
|
|
2
|
+
import { i as kib, n as documentSizeDataSchema, r as documentSizeRule } from "../../../../../document-size.js";
|
|
3
|
+
|
|
4
|
+
//#region src/audit/catalog/categories/performance/rules/html-document-size.ts
|
|
5
|
+
var html_document_size_default = documentSizeRule({
|
|
6
|
+
label: "HTML size",
|
|
7
|
+
budget: 350 * 1024,
|
|
8
|
+
value: (size) => size.htmlBytes,
|
|
9
|
+
format: kib,
|
|
10
|
+
evidence: () => null,
|
|
11
|
+
decideFailures: true
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { documentSizeDataSchema as dataSchema, html_document_size_default as default };
|