webperf-snippets 0.4.0 → 0.4.1

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 CHANGED
@@ -77,6 +77,8 @@ npx webperf-snippets https://web.dev --snippet Interaction/Interactions --intera
77
77
 
78
78
  The Interaction snippets (`Interactions`, `Input-Latency-Breakdown`, `LongTask`, `Forced-Synchronous-Layout`...) are installed before the first step and read after the last one, so their result holds what the steps caused.
79
79
 
80
+ The CLI waits for the browser to report the timing of the last interaction before it reads the snippets. A script that ends right after a click still reads the page before anything the click sets in motion later has happened, such as a request or an animation, so end it with a `wait` step when you want to measure that.
81
+
80
82
  Interaction audit (long tasks, long animation frames, scroll, layout shifts, forced layouts, interaction latency). It scrolls the page, or runs your `--interact-script`; the steps that need a click are skipped when the script has none:
81
83
 
82
84
  ```bash
@@ -132,7 +134,7 @@ Without `--report-to` the CLI makes no external calls. With it, the results are
132
134
  "timestamp": "2026-05-06T10:00:00.000Z",
133
135
  "navMs": 1240,
134
136
  "results": [],
135
- "meta": { "viewport": "mobile", "waitMs": 3000, "cli_version": "0.4.0" }
137
+ "meta": { "viewport": "mobile", "waitMs": 3000, "cli_version": "0.4.1" }
136
138
  }
137
139
  ```
138
140
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webperf-snippets",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Run curated WebPerf Snippets headlessly via Playwright. Diagnose Core Web Vitals beyond what Lighthouse exposes.",
5
5
  "type": "module",
6
6
  "bin": "./src/bin.js",
@@ -38,3 +38,30 @@ export async function runInteractions(page, source) {
38
38
  await executeStep(page, step);
39
39
  }
40
40
  }
41
+
42
+ // The browser reports the timing of an event after the frame that follows it, and an observer gets it
43
+ // in a task after that frame. Reading a snippet right after the last step can miss that step, so this
44
+ // waits for two frames and a task, which takes a few milliseconds on a page that renders. A page that
45
+ // never renders a frame stops the wait at `limitMs`, and a page that is gone is not an error here: the
46
+ // evaluation that follows reports it.
47
+ export async function settle(page, limitMs = 1000) {
48
+ try {
49
+ await page.evaluate(
50
+ (limit) =>
51
+ new Promise((resolve) => {
52
+ const timer = setTimeout(resolve, limit);
53
+ requestAnimationFrame(() =>
54
+ requestAnimationFrame(() =>
55
+ setTimeout(() => {
56
+ clearTimeout(timer);
57
+ resolve();
58
+ }, 0)
59
+ )
60
+ );
61
+ }),
62
+ limitMs
63
+ );
64
+ } catch {
65
+ // the page navigated or closed
66
+ }
67
+ }
package/src/runner.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { chromium } from "playwright";
2
2
  import { loadSnippet } from "./load-snippet.js";
3
- import { runInteractions, loadInteractionSteps, isInputStep } from "./interactions.js";
3
+ import { runInteractions, loadInteractionSteps, isInputStep, settle } from "./interactions.js";
4
4
  import { isTrackingSnippet } from "./tracking.js";
5
5
  import { nextSteps } from "./decision-tree.js";
6
6
 
@@ -42,14 +42,16 @@ async function evaluateItems(page, items) {
42
42
 
43
43
  // Runs the items around a set of interactions. A tracking snippet is installed first, so it sees
44
44
  // the interactions, and answers through its getDataFn afterwards. The other items run after the
45
- // interactions, as before. Results keep the order of the items. Without interactions every item
46
- // runs once, as it always did.
45
+ // interactions, as before. The browser reports the timing of an interaction after the frame that
46
+ // follows it, so the runner lets it settle before it reads anything. Results keep the order of the
47
+ // items. Without interactions every item runs once, as it always did.
47
48
  async function evaluateAroundInteractions(page, items, interactions) {
48
49
  if (!interactions) return evaluateItems(page, items);
49
50
 
50
51
  const tracking = items.filter((item) => isTrackingSnippet(item.path ?? "", item.source));
51
52
  const installed = await evaluateItems(page, tracking);
52
53
  await runInteractions(page, interactions);
54
+ await settle(page);
53
55
  const others = await evaluateItems(
54
56
  page,
55
57
  items.filter((item) => !tracking.includes(item))
package/src/tracking.js CHANGED
@@ -1,6 +1,6 @@
1
- // A tracking snippet installs observers and answers later through a window function named in
2
- // its result (getDataFn). The Interaction snippets work this way. CLS and INP also name a
3
- // getDataFn but answer with their value in one go, so only the Interaction category counts.
1
+ // A tracking snippet installs observers and answers later through a window function named in its
2
+ // result (getDataFn). The Interaction snippets work this way, and so does INP, whose value only
3
+ // comes out of getINP(). CLS also names a getDataFn but answers with its value in one go.
4
4
  export function isTrackingSnippet(path, source) {
5
- return path.startsWith("Interaction/") && source.includes("getDataFn");
5
+ return (path.startsWith("Interaction/") || path === "CoreWebVitals/INP") && source.includes("getDataFn");
6
6
  }