webperf-snippets 0.1.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 +139 -0
- package/package.json +54 -0
- package/snippets/CoreWebVitals/CLS.js +88 -0
- package/snippets/CoreWebVitals/INP.js +432 -0
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +198 -0
- package/snippets/CoreWebVitals/LCP-Subparts.js +286 -0
- package/snippets/CoreWebVitals/LCP-Trail.js +184 -0
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +228 -0
- package/snippets/CoreWebVitals/LCP.js +135 -0
- package/snippets/CoreWebVitals/WORKFLOWS.md +203 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +73 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +191 -0
- package/snippets/Interaction/Forced-Synchronous-Layout.js +286 -0
- package/snippets/Interaction/Input-Latency-Breakdown.js +199 -0
- package/snippets/Interaction/Interactions.js +220 -0
- package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +266 -0
- package/snippets/Interaction/Long-Animation-Frames-Helpers.js +397 -0
- package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +137 -0
- package/snippets/Interaction/Long-Animation-Frames.js +298 -0
- package/snippets/Interaction/LongTask.js +203 -0
- package/snippets/Interaction/Scroll-Performance.js +359 -0
- package/snippets/Interaction/WORKFLOWS.md +205 -0
- package/snippets/Loading/Back-Forward-Cache.js +473 -0
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +512 -0
- package/snippets/Loading/Cache-Strategy-Analysis.js +808 -0
- package/snippets/Loading/Client-Side-Redirect-Detection.js +337 -0
- package/snippets/Loading/Content-Visibility.js +352 -0
- package/snippets/Loading/Critical-CSS-Detection.js +329 -0
- package/snippets/Loading/Event-Processing-Time.js +179 -0
- package/snippets/Loading/FCP.js +101 -0
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +264 -0
- package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +155 -0
- package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +300 -0
- package/snippets/Loading/Find-render-blocking-resources.js +161 -0
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +269 -0
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +295 -0
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +317 -0
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +234 -0
- package/snippets/Loading/Inline-Script-Info-and-Size.js +323 -0
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +336 -0
- package/snippets/Loading/Prefetch-Resource-Validation.js +456 -0
- package/snippets/Loading/Priority-Hints-Audit.js +336 -0
- package/snippets/Loading/Resource-Hints-Validation.js +503 -0
- package/snippets/Loading/Resource-Hints.js +400 -0
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +364 -0
- package/snippets/Loading/Script-Loading.js +332 -0
- package/snippets/Loading/Service-Worker-Analysis.js +192 -0
- package/snippets/Loading/TTFB-Resources.js +115 -0
- package/snippets/Loading/TTFB-Sub-Parts.js +117 -0
- package/snippets/Loading/TTFB.js +37 -0
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +394 -0
- package/snippets/Loading/WORKFLOWS.md +163 -0
- package/snippets/Media/Image-Element-Audit.js +350 -0
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +225 -0
- package/snippets/Media/Video-Element-Audit.js +282 -0
- package/snippets/Media/WORKFLOWS.md +190 -0
- package/snippets/README.md +98 -0
- package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +103 -0
- package/snippets/Resources/WORKFLOWS.md +173 -0
- package/src/bin.js +229 -0
- package/src/decision-tree.js +19 -0
- package/src/load-snippet.js +23 -0
- package/src/reporters/human.js +179 -0
- package/src/reporters/json.js +3 -0
- package/src/runner.js +61 -0
- package/src/workflows/audit.js +21 -0
- package/src/workflows/cwv.js +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# webperf-snippets CLI
|
|
2
|
+
|
|
3
|
+
Run curated [WebPerf Snippets](https://webperf-snippets.nucliweb.net) headlessly via Playwright. Diagnose Core Web Vitals beyond what Lighthouse exposes and gate CI on real performance budgets.
|
|
4
|
+
|
|
5
|
+
> **Status:** v0.1 (MVP). Core Web Vitals workflow only. See [Roadmap](#roadmap) for what's next.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Lighthouse gives you a score. The DevTools snippets give you the *diagnosis* — TTFB / Resource Load Delay / Element Render Delay sub-parts, LoAF script attribution, render-blocking resources, etc. This CLI runs the same curated snippets in a headless browser so you can:
|
|
10
|
+
|
|
11
|
+
- Diagnose LCP regressions in CI without copy-pasting into DevTools.
|
|
12
|
+
- Gate pull requests on real performance budgets.
|
|
13
|
+
- Automate the snippets you already run by hand.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Playwright is a peer dependency. Install both, plus the chromium browser:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install --save-dev webperf-snippets playwright
|
|
21
|
+
npx playwright install chromium
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx webperf-snippets <url> [options]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Examples
|
|
31
|
+
|
|
32
|
+
Run the default Core Web Vitals workflow (LCP + CLS, plus LCP-Subparts if LCP > 2.5s):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx webperf-snippets https://web.dev
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
JSON output (for piping into `jq` or CI):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx webperf-snippets https://web.dev --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Single snippet:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx webperf-snippets https://web.dev --snippet LCP-Subparts
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
CI gating:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx webperf-snippets https://web.dev --budget-lcp 2500 --budget-cls 0.1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Options
|
|
57
|
+
|
|
58
|
+
| Option | Description |
|
|
59
|
+
| --------------------- | ---------------------------------------------------------- |
|
|
60
|
+
| `--workflow <name>` | Workflow to run. Default: `core-web-vitals`. |
|
|
61
|
+
| `--snippet <name>` | Run a single snippet (`LCP`, `CLS`, or `Category/Name`). |
|
|
62
|
+
| `--json` | Output JSON instead of formatted text. |
|
|
63
|
+
| `--wait <ms>` | Post-load wait before evaluating snippets. Default: `3000`.|
|
|
64
|
+
| `--budget-lcp <ms>` | Exit `1` if LCP exceeds this value. |
|
|
65
|
+
| `--budget-cls <score>`| Exit `1` if CLS exceeds this value. |
|
|
66
|
+
| `--headed` | Show the browser window (debug). |
|
|
67
|
+
| `-h, --help` | Show help. |
|
|
68
|
+
|
|
69
|
+
### Exit codes
|
|
70
|
+
|
|
71
|
+
| Code | Meaning |
|
|
72
|
+
| ---- | --------------------------------------------- |
|
|
73
|
+
| `0` | All checks passed. |
|
|
74
|
+
| `1` | Budget violation, or a snippet errored. |
|
|
75
|
+
| `2` | Usage error (missing URL, unknown workflow). |
|
|
76
|
+
|
|
77
|
+
## CI example
|
|
78
|
+
|
|
79
|
+
GitHub Actions, fail the PR if LCP exceeds 2.5s:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
- run: |
|
|
83
|
+
npm install --no-save webperf-snippets playwright
|
|
84
|
+
npx playwright install --with-deps chromium
|
|
85
|
+
npx webperf-snippets https://staging.web.dev --budget-lcp 2500 --budget-cls 0.1
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Publishing
|
|
89
|
+
|
|
90
|
+
The CLI package is published to npm via a tag-based workflow. Publishing is explicit and intentional — it only happens when a `cli-v*` tag is pushed.
|
|
91
|
+
|
|
92
|
+
### Release steps
|
|
93
|
+
|
|
94
|
+
1. Bump the version in `cli/package.json`.
|
|
95
|
+
2. Commit the version change.
|
|
96
|
+
3. Tag and push:
|
|
97
|
+
```bash
|
|
98
|
+
git tag cli-v0.2.0
|
|
99
|
+
git push origin cli-v0.2.0
|
|
100
|
+
```
|
|
101
|
+
4. The `publish-cli` CI job runs, executes the full test suite, and publishes to npm.
|
|
102
|
+
|
|
103
|
+
### Why tag-based and not path-based
|
|
104
|
+
|
|
105
|
+
An alternative is to publish automatically on every push to `main` that touches `cli/`, using a version check to skip republishes. Tag-based publishing was chosen instead because it keeps releases deliberate — a passing CI on `main` does not mean the package is ready to ship, and a tag communicates that intent explicitly.
|
|
106
|
+
|
|
107
|
+
### Access control
|
|
108
|
+
|
|
109
|
+
Tag protection rules restrict who can push `cli-v*` tags. Configure them under **Settings → Rules → New ruleset** in the repository, targeting the `cli-v*` tag pattern and limiting push access to admins or maintainers. This ensures only authorized collaborators can trigger a publish.
|
|
110
|
+
|
|
111
|
+
### Required secret
|
|
112
|
+
|
|
113
|
+
The `NPM_TOKEN` secret must be set in the repository settings with publish access to the `webperf-snippets` npm package.
|
|
114
|
+
|
|
115
|
+
## Known limitations (v0.1)
|
|
116
|
+
|
|
117
|
+
- **No INP**: INP requires real user interactions. v0.2 will support synthetic interaction scripts.
|
|
118
|
+
- **CLS in headless is conservative**: layout shifts that only happen on scroll are missed unless you script the scroll.
|
|
119
|
+
- **First navigation only**: each `webperf-snippets` invocation runs one URL. SPAs need the post-route URL passed directly.
|
|
120
|
+
- **Decision-tree follow-ups re-navigate**: when a follow-up snippet fires (e.g. LCP-Subparts), the page is loaded again. v0.2 will share a single page session.
|
|
121
|
+
|
|
122
|
+
## Roadmap
|
|
123
|
+
|
|
124
|
+
- v0.2: Loading workflow (TTFB, FCP, render-blocking, scripts, fonts), shared page session, synthetic interactions for INP.
|
|
125
|
+
- v0.3: Markdown reporter for PR comments, GitHub Action wrapper.
|
|
126
|
+
- v0.4: Auth flows (login + measure logged-in pages), CrUX field-data enrichment.
|
|
127
|
+
|
|
128
|
+
## How it works
|
|
129
|
+
|
|
130
|
+
1. Launches headless chromium via Playwright.
|
|
131
|
+
2. Pre-registers `PerformanceObserver`s for LCP and layout-shift before navigation (Chrome doesn't expose these via `getEntriesByType` without a buffered observer, so the runner shims it).
|
|
132
|
+
3. Navigates, waits for the page to settle.
|
|
133
|
+
4. Evaluates each snippet's IIFE in the page context, capturing the returned object.
|
|
134
|
+
5. Applies the workflow's decision tree to enqueue follow-up snippets.
|
|
135
|
+
6. Renders results (human or JSON) and exits with an appropriate code.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT — see [LICENSE](../LICENSE).
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "webperf-snippets",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run curated WebPerf Snippets headlessly via Playwright. Diagnose Core Web Vitals beyond what Lighthouse exposes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "./src/bin.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"snippets",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:unit": "vitest run tests/unit",
|
|
16
|
+
"test:e2e": "vitest run tests/e2e"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"vitest": "^2.0.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.12.0"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"webperf",
|
|
26
|
+
"performance",
|
|
27
|
+
"core-web-vitals",
|
|
28
|
+
"lcp",
|
|
29
|
+
"cls",
|
|
30
|
+
"inp",
|
|
31
|
+
"playwright",
|
|
32
|
+
"cli",
|
|
33
|
+
"ci"
|
|
34
|
+
],
|
|
35
|
+
"author": "Joan Leon | @nucliweb",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/nucliweb/webperf-snippets.git",
|
|
43
|
+
"directory": "cli"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://webperf-snippets.nucliweb.net",
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"playwright": ">=1.40.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"playwright": {
|
|
51
|
+
"optional": false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// CLS Quick Check
|
|
2
|
+
// https://webperf-snippets.nucliweb.net
|
|
3
|
+
|
|
4
|
+
(async () => {
|
|
5
|
+
let cls = 0;
|
|
6
|
+
|
|
7
|
+
const valueToRating = (score) =>
|
|
8
|
+
score <= 0.1 ? "good" : score <= 0.25 ? "needs-improvement" : "poor";
|
|
9
|
+
|
|
10
|
+
const RATING = {
|
|
11
|
+
good: { icon: "🟢", color: "#0CCE6A" },
|
|
12
|
+
"needs-improvement": { icon: "🟡", color: "#FFA400" },
|
|
13
|
+
poor: { icon: "🔴", color: "#FF4E42" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const logCLS = () => {
|
|
17
|
+
const rating = valueToRating(cls);
|
|
18
|
+
const { icon, color } = RATING[rating];
|
|
19
|
+
console.log(
|
|
20
|
+
`%cCLS: ${icon} ${cls.toFixed(4)} (${rating})`,
|
|
21
|
+
`color: ${color}; font-weight: bold; font-size: 14px;`
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const observer = new PerformanceObserver((list) => {
|
|
26
|
+
for (const entry of list.getEntries()) {
|
|
27
|
+
if (!entry.hadRecentInput) {
|
|
28
|
+
cls += entry.value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
logCLS();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
observer.observe({ type: "layout-shift", buffered: true });
|
|
35
|
+
|
|
36
|
+
// Update on visibility change (final CLS)
|
|
37
|
+
document.addEventListener("visibilitychange", () => {
|
|
38
|
+
if (document.visibilityState === "hidden") {
|
|
39
|
+
observer.takeRecords();
|
|
40
|
+
console.log("%c📊 Final CLS (on page hide):", "font-weight: bold;");
|
|
41
|
+
logCLS();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Expose function for manual check
|
|
46
|
+
window.getCLS = () => {
|
|
47
|
+
logCLS();
|
|
48
|
+
const rating = valueToRating(cls);
|
|
49
|
+
return {
|
|
50
|
+
script: "CLS",
|
|
51
|
+
status: "ok",
|
|
52
|
+
metric: "CLS",
|
|
53
|
+
value: Math.round(cls * 10000) / 10000,
|
|
54
|
+
unit: "score",
|
|
55
|
+
rating,
|
|
56
|
+
thresholds: { good: 0.1, needsImprovement: 0.25 },
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
console.log(
|
|
61
|
+
" Call %cgetCLS()%c anytime to check current value.",
|
|
62
|
+
"font-family: monospace; background: #f3f4f6; padding: 2px 4px;",
|
|
63
|
+
""
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Return for agent — collect via buffered observer (getEntriesByType does not
|
|
67
|
+
// expose layout-shift entries in Chrome without an active observer).
|
|
68
|
+
const clsSync = await new Promise((resolve) => {
|
|
69
|
+
let sum = 0;
|
|
70
|
+
const obs = new PerformanceObserver((list) => {
|
|
71
|
+
for (const e of list.getEntries()) if (!e.hadRecentInput) sum += e.value;
|
|
72
|
+
});
|
|
73
|
+
obs.observe({ type: "layout-shift", buffered: true });
|
|
74
|
+
setTimeout(() => { obs.disconnect(); resolve(sum); }, 100);
|
|
75
|
+
});
|
|
76
|
+
const clsRating = valueToRating(clsSync);
|
|
77
|
+
return {
|
|
78
|
+
script: "CLS",
|
|
79
|
+
status: "ok",
|
|
80
|
+
metric: "CLS",
|
|
81
|
+
value: Math.round(clsSync * 10000) / 10000,
|
|
82
|
+
unit: "score",
|
|
83
|
+
rating: clsRating,
|
|
84
|
+
thresholds: { good: 0.1, needsImprovement: 0.25 },
|
|
85
|
+
message: "CLS tracking active. Call getCLS() for updated value after page interactions.",
|
|
86
|
+
getDataFn: "getCLS",
|
|
87
|
+
};
|
|
88
|
+
})();
|