Last active
October 15, 2025 15:06
-
-
Save cloudydaiyz/923ca44bdb0a1ff3434a9360967025a6 to your computer and use it in GitHub Desktop.
Using the web-vitals library in playwright browsers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { test, expect } from "@playwright/test"; | |
| import type * as webVitals from 'web-vitals'; | |
| import path from 'path'; | |
| import fs from 'fs'; | |
| declare global { | |
| interface Window { | |
| webVitals: typeof webVitals; | |
| validateMetric: (name: string, good: boolean) => void; | |
| } | |
| } | |
| test.describe("HackerNews/newest", () => { | |
| // Web vitals are collected differently for each browser. | |
| // Some metrics may be collected inconsistently or not at all. | |
| test('should have good web vitals', async ({ page }) => { | |
| // Set up page listeners | |
| page.on("console", async (msg) => { | |
| if (msg.type() === "trace" || msg.type() === "error") { | |
| console.log(msg.text()); | |
| } | |
| }); | |
| const badMetrics: string[] = []; | |
| function validateMetric(name: string, good: boolean) { | |
| console.log(`${name} valid: ${good}`); | |
| if(!good) { | |
| badMetrics.push(name); | |
| } | |
| } | |
| page.exposeFunction("validateMetric", validateMetric); | |
| // Using classic script to get webVitals global namespace | |
| const scriptPath = path.join(__dirname, 'node_modules/web-vitals/dist/web-vitals.attribution.iife.js'); | |
| // Read the web-vitals script content at runtime | |
| const scriptBody = fs.readFileSync(scriptPath, 'utf8'); | |
| /* | |
| * Inject the script into the page. | |
| * Make sure to use the raw script content instead of creating a script with the src set to `webVitalsUrl`. | |
| * Otherwise, the external script may get blocked on certain websites. | |
| */ | |
| await page.addInitScript( | |
| async ([scriptBody]) => { | |
| window.addEventListener("DOMContentLoaded", async () => { | |
| const script = document.createElement("script"); | |
| script.text = scriptBody; | |
| try { | |
| document.head.appendChild(script); | |
| } catch (e) { | |
| console.error("Error when initializing injected CWV script"); | |
| console.error(e); | |
| } | |
| // From GoogleChrome/web-vitals: | |
| // > Note that some of these metrics will not report until the user has interacted with the page, | |
| // > switched tabs, or the page starts to unload. | |
| // See: https://github.com/GoogleChrome/web-vitals#:~:text=Note%20that%20some%20of%20these%20metrics%20will%20not%20report%20until%20the%20user%20has%20interacted%20with%20the%20page%2C%20switched%20tabs%2C%20or%20the%20page%20starts%20to%20unload. | |
| function checkMetric(m: webVitals.Metric) { | |
| console.trace(JSON.stringify(m, null, 4)); | |
| if(m.rating !== "good") { | |
| window.validateMetric(m.name, false); | |
| return; | |
| } | |
| window.validateMetric(m.name, true); | |
| } | |
| window.webVitals.onCLS(checkMetric); | |
| window.webVitals.onFCP(checkMetric); | |
| window.webVitals.onFID(checkMetric); // NOTE: will be removed from library soon | |
| window.webVitals.onINP(checkMetric); | |
| window.webVitals.onLCP(checkMetric); | |
| window.webVitals.onTTFB(checkMetric); | |
| console.log("Initialized web vital monitors"); | |
| }); | |
| }, | |
| [scriptBody] | |
| ); | |
| // Go to Hacker News, record FCP and TTFB | |
| await page.goto("https://news.ycombinator.com/newest"); | |
| // Page click to record LCP and FID | |
| await page.waitForLoadState('domcontentloaded'); | |
| console.log("Page click"); | |
| await page.locator("input[type=text]").click(); | |
| // Manually obtain navigation event metrics for page load time | |
| // - NOTE: There is only one PerformanceNavigationTiming object in the performance timeline | |
| // See: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming | |
| const [navTiming] = await page.evaluate(() => | |
| window.performance.getEntriesByType("navigation").map((e) => e.toJSON()) | |
| ); | |
| console.debug("Navigation Entry:", navTiming); | |
| console.log("Page load time:", navTiming.loadEventEnd); | |
| validateMetric("Page load", navTiming.loadEventEnd < 2000); | |
| // Page unload to record INP and CLS | |
| console.log("Page unload"); | |
| await page.close(); | |
| expect(badMetrics).toHaveLength(0); | |
| }); | |
| }); |
Author
@dilirity Agreed. I've updated the gist to include these lines and removed the unnecessary script injection. Thank you for pointing this out!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hello there!
Thank you so much for this gist. It saved me a ton of time!
One thing I noticed is that the
await page.addInitScript({ content: scriptBody });on line 43 is not necessary, as lines 44 through 79 do the actual adding of the code to the page.Also - instead of loading the code via CDN, I read the contents from the package itself and loaded those to the page. That should be faster than loading via CDN as it's inline code: