{"id":13583716,"url":"https://github.com/addyosmani/puppeteer-webperf","last_synced_at":"2025-04-08T08:12:34.453Z","repository":{"id":41438862,"uuid":"258936782","full_name":"addyosmani/puppeteer-webperf","owner":"addyosmani","description":"Automating Web Performance testing with Puppeteer 🎪","archived":false,"fork":false,"pushed_at":"2023-01-18T10:55:29.000Z","size":6112,"stargazers_count":1846,"open_issues_count":19,"forks_count":90,"subscribers_count":34,"default_branch":"master","last_synced_at":"2025-04-01T05:33:39.137Z","etag":null,"topics":["chrome","chrome-devtools","devtools","lighthouse","puppeteer","puppeteer-demo","web-performance"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/addyosmani.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-26T04:12:41.000Z","updated_at":"2025-03-17T14:44:18.000Z","dependencies_parsed_at":"2023-01-26T23:30:55.144Z","dependency_job_id":null,"html_url":"https://github.com/addyosmani/puppeteer-webperf","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/addyosmani%2Fpuppeteer-webperf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/addyosmani%2Fpuppeteer-webperf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/addyosmani%2Fpuppeteer-webperf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/addyosmani%2Fpuppeteer-webperf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/addyosmani","download_url":"https://codeload.github.com/addyosmani/puppeteer-webperf/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247801169,"owners_count":20998339,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chrome","chrome-devtools","devtools","lighthouse","puppeteer","puppeteer-demo","web-performance"],"created_at":"2024-08-01T15:03:43.319Z","updated_at":"2025-04-08T08:12:34.424Z","avatar_url":"https://github.com/addyosmani.png","language":"JavaScript","funding_links":[],"categories":["Metrics Monitor","JavaScript","JS/ES6"],"sub_categories":["Meetups"],"readme":"\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://user-images.githubusercontent.com/110953/80448571-91919100-88d1-11ea-936a-a8fb1785311e.jpg\" alt=\"Puppeteer WebPerf logo\" width=\"70%\"/\u003e\n\u003ch1\u003eAutomating Web Perf measurement with Puppeteer\u003c/h1\u003e\n\u003c/p\u003e\n\n🕹 \u003ca href=\"https://pptr.dev\"\u003ePuppeteer\u003c/a\u003e is a Node library which provides a high-level API to control headless Chrome or Chromium over the \u003ca href=\"https://chromedevtools.github.io/devtools-protocol/\"\u003eDevTools Protocol\u003c/a\u003e. This repository has recipes for automating Web Performance measurement with Puppeteer.\n\n## Table Of Contents\n\n* [Get a DevTools performance trace for a page load](#devtools-profile)\n* [Get a DevTools trace with screenshots](#devtools-screenshots)\n* [Get a DevTools trace and extract filmstrip screenshots](#devtools-trace-screenshots)\n* [Get a DevTools trace for a user interaction](#devtools-interaction)\n* [Get Runtime performance metrics](#runtime-perf-metrics)\n* [Generate a Lighthouse report](#lighthouse-report)\n* [Extract Lighthouse performance metrics](#lighthouse-metrics)\n* [Emulate a slow network](#throttle-network)\n* [Emulate a slow network and CPU](#throttle-network-cpu)\n* [Test your site renders with JavaScript disabled](#javascript-disabled)\n* [Get Navigation Timing API metrics](#navigation-timing)\n* [Measure First Paint and First Contentful Paint](#first-contentful-paint)\n* [Measure Largest Contentful Paint w/PerformanceObserver](#largest-contentful-paint)\n* [Measure Cumulative Layout Shift w/PerformanceObserver](#cumulative-layout-shift)\n* [Measure SPA metrics with Next.js](#nextjs-metrics)\n* [Get DevTools-specific metrics: Frames Per Second](#devtools-frame-rate)\n* [Measure memory leaks](#measure-memory-leaks)\n* [Override requests with Request Interception](#request-interception)\n* [Block third-party domains](#block-third-parties)\n* [Code Coverage for JavaScript and CSS](#code-coverage)\n* [Save network requests to a HAR file](#har-file)\n\n\u003ch3 id=\"devtools-profile\"\u003eGet a DevTools performance trace for a page load\u003c/h3\u003e\n\nPuppeteer API: [tracing.start()](https://pptr.dev/#?product=Puppeteer\u0026show=api-tracingstartoptions)\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  // Drag and drop this JSON file to the DevTools Performance panel!\n  await page.tracing.start({path: 'profile.json'});\n  await page.goto('https://pptr.dev');\n  await page.tracing.stop();\n  await browser.close();\n})();\n```\n\n[Source](devtools-profile.js)\n\n\u003cimg src=\"/assets/images/Performance-tracing0.png\" alt=\"Screenshot of a DevTools performance profile from loading and rendering a page\"/\u003e\n\n\u003ch3 id=\"devtools-screenshots\"\u003eGet a DevTools trace with screenshots\u003c/h3\u003e\n\nPuppeteer API: [tracing.start()](https://pptr.dev/#?product=Puppeteer\u0026show=api-tracingstartoptions)\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n    const browser = await puppeteer.launch();\n    const page = await browser.newPage();\n    // Drag and drop this JSON file to the DevTools Performance panel!\n    await page.tracing.start({ path: 'profile.json', screenshots: true });\n    await page.goto('https://pptr.dev');\n    await page.tracing.stop();\n    await browser.close();\n})();\n```\n\n[Source](devtools-screenshots.js)\n\n\u003cimg src=\"/assets/images/Performance-tracing2.png\" alt=\"DevTools screenshots in the performance panel\"/\u003e\n\n\u003ch3 id=\"devtools-trace-screenshots\"\u003eGet a DevTools trace and extract filmstrip screenshots\u003c/h3\u003e\n\nIf you would like to record a performance trace and extract filmstrip screenshots from that trace to a local directory, the below snippet should do the trick. It works by filtering trace events for screenshot entries. \n\n```js\nconst puppeteer = require('puppeteer');\nconst fs = require('fs');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n\n  await page.tracing.start({ screenshots: true, path: 'trace.json' });\n  await page.goto('https://netflix.com', { timeout: 60000 });\n  await page.tracing.stop();\n\n  // Extract data from the trace\n  const tracing = JSON.parse(fs.readFileSync('./trace.json', 'utf8'));\n  const traceScreenshots = tracing.traceEvents.filter(x =\u003e (\n      x.cat === 'disabled-by-default-devtools.screenshot' \u0026\u0026\n      x.name === 'Screenshot' \u0026\u0026\n      typeof x.args !== 'undefined' \u0026\u0026\n      typeof x.args.snapshot !== 'undefined'\n  ));\n\n  traceScreenshots.forEach(function(snap, index) {\n    fs.writeFile(`trace-screenshot-${index}.png`, snap.args.snapshot, 'base64', function(err) {\n      if (err) {\n        console.log('writeFile error', err);\n      }\n    });\n  });\n\n  await browser.close();\n})();\n```\n\n[Source](devtools-trace-screenshots.js)\n\n![](/assets/images/trace-screenshots@2x.jpg)\n\n\n\u003ch3 id=\"devtools-interaction\"\u003eGet a DevTools trace for a user interaction\u003c/h3\u003e\n\nPuppeteer API: [page.click()](https://pptr.dev/#?product=Puppeteer\u0026show=api-pageclickselector-options)\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  const navigationPromise = page.waitForNavigation();\n  await page.goto('https://pptr.dev/#?product=Puppeteer\u0026version=v2.1.1\u0026show=outline');\n  await page.setViewport({ width: 1440, height: 714 });\n  \n  await navigationPromise;\n  const selector = 'body \u003e sidebar-component \u003e sidebar-item:nth-child(3) \u003e .pptr-sidebar-item';\n  await page.waitForSelector(selector);\n  await page.tracing.start({path: 'trace.json', screenshots: true});\n  await page.click(selector);\n  await page.tracing.stop();\n  \n  await browser.close();\n})();\n```\n[Source](devtools-interaction.js)\n\n![](/assets/images/interaction@2x.png)\n\n\u003ch3 id=\"runtime-perf-metrics\"\u003eGet Runtime performance metrics\u003c/h3\u003e\n\nThe `page.metrics()` returns runtime metrics from the Chrome DevTools Protocol Performance.getMetrics() method, such as layout  duration, recalc-style durations and JS event listeners.\n\nPuppeteer API: [metrics()](https://pptr.dev/#?product=Puppeteer\u0026show=api-pagemetrics)\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n    const browser = await puppeteer.launch();\n    const page = await browser.newPage();\n    await page.goto('https://pptr.dev');\n\n    const metrics = await page.metrics();\n    console.info(metrics);\n\n    await browser.close();\n})();\n```\n\n[Source](runtime-perf-metrics.js)\n\n\u003cimg src=\"/assets/images/Performance-tracing8.png\" alt=\"Runtime performance metrics shown in the terminal\"/\u003e\n\n\u003ch3 id=\"lighthouse-report\"\u003eGenerate a Lighthouse report\u003c/h3\u003e\n\n💡🏠 Lighthouse is an engine for analyzing web apps and web pages, collecting modern performance metrics and insights on developer best practices. It's available in the Chrome DevTools, PageSpeed Insights, a CLI and as a consumable module.\n\nGenerate a Lighthouse report for a URL and output it to a local HTML file. For more details, see the official guide to [using Puppeteer with Lighthouse](https://github.com/GoogleChrome/lighthouse/blob/master/docs/puppeteer.md).\n\nPuppeteer API: [connect()](https://pptr.dev/#?product=Puppeteer\u0026show=api-puppeteerconnectoptions)\n\n```js\nconst fs = require('fs');\nconst lighthouse = require('lighthouse');\nconst puppeteer = require('puppeteer');\n\nconst chromeLauncher = require('chrome-launcher');\nconst reportGenerator = require('lighthouse/lighthouse-core/report/report-generator');\nconst request = require('request');\nconst util = require('util');\n\nconst options = {\n  logLevel: 'info',\n  disableDeviceEmulation: true,\n  chromeFlags: ['--disable-mobile-emulation']\n};\n\nasync function lighthouseFromPuppeteer(url, options, config = null) {\n  // Launch chrome using chrome-launcher\n  const chrome = await chromeLauncher.launch(options);\n  options.port = chrome.port;\n\n  // Connect chrome-launcher to puppeteer\n  const resp = await util.promisify(request)(`http://localhost:${options.port}/json/version`);\n  const { webSocketDebuggerUrl } = JSON.parse(resp.body);\n  const browser = await puppeteer.connect({ browserWSEndpoint: webSocketDebuggerUrl });\n\n  // Run Lighthouse\n  const { lhr } = await lighthouse(url, options, config);\n  await browser.disconnect();\n  await chrome.kill();\n\n  const html = reportGenerator.generateReport(lhr, 'html');\n  fs.writeFile('report.html', html, function (err) {\n    if (err) throw err;\n  });\n}\n\nlighthouseFromPuppeteer(\"https://pptr.dev\", options);\n```\n\n[Source](lighthouse-report.js)\n\n\u003cimg src=\"/assets/images/Performance-tracing3.png\" alt=\"Lighthouse report generation from Puppeteer\"/\u003e\n\n\u003ch3 id=\"lighthouse-metrics\"\u003eExtract Lighthouse performance metrics\u003c/h3\u003e\n\n[Lighthouse](https://developers.google.com/web/tools/lighthouse/) exposes a number of [user-centric performance metrics](https://web.dev/user-centric-performance-metrics/). It's possible to pluck these metrics values out from the JSON response, as demonstrated below.\n\n```js\nconst fs = require('fs');\nconst lighthouse = require('lighthouse');\nconst puppeteer = require('puppeteer');\n\nconst chromeLauncher = require('chrome-launcher');\nconst reportGenerator = require('lighthouse/lighthouse-core/report/report-generator');\nconst request = require('request');\nconst util = require('util');\n\nconst options = {\n  logLevel: 'info',\n  disableDeviceEmulation: true,\n  chromeFlags: ['--disable-mobile-emulation']\n};\n\nasync function lighthouseFromPuppeteer(url, options, config = null) {\n  // Launch chrome using chrome-launcher\n  const chrome = await chromeLauncher.launch(options);\n  options.port = chrome.port;\n\n  // Connect chrome-launcher to puppeteer\n  const resp = await util.promisify(request)(`http://localhost:${options.port}/json/version`);\n  const { webSocketDebuggerUrl } = JSON.parse(resp.body);\n  const browser = await puppeteer.connect({ browserWSEndpoint: webSocketDebuggerUrl });\n\n  // Run Lighthouse\n  const { lhr } = await lighthouse(url, options, config);\n  await browser.disconnect();\n  await chrome.kill();\n\n  const json = reportGenerator.generateReport(lhr, 'json');\n\n  const audits = JSON.parse(json).audits; // Lighthouse audits\n  const first_contentful_paint = audits['first-contentful-paint'].displayValue;\n  const total_blocking_time = audits['total-blocking-time'].displayValue;\n  const time_to_interactive = audits['interactive'].displayValue;\n\n  console.log(`\\n\n     Lighthouse metrics: \n     🎨 First Contentful Paint: ${first_contentful_paint}, \n     ⌛️ Total Blocking Time: ${total_blocking_time},\n     👆 Time To Interactive: ${time_to_interactive}`);\n}\n\nlighthouseFromPuppeteer(\"https://bbc.com\", options);\n```\n[Source](lighthouse-metrics.js)\n\n![](/assets/images/lh-metrics@2x.png)\n\n\u003ch3 id=\"throttle-network\"\u003eEmulate a slow network\u003c/h3\u003e\n\nIf you need to throttle the network connection, use [Puppeteer’s `page.emulateNetworkConditions` API](https://github.com/puppeteer/puppeteer/blob/v8.0.0/docs/api.md#pageemulatenetworkconditionsnetworkconditions).\n\n🚨 Real network performance can be impacted by latency to towers, traffic patterns and the current radio activity. The \u003ca href=\"https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md\"\u003eLighthouse guide to network throttling\u003c/a\u003e covers in more detail what the differences are between simulated, request-level and packet-level throttling.\n\nYou can use \u003ca href=\"https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#using-lighthouse-with-comcast\"\u003eLighthouse with the comcast module\u003c/a\u003e for packet-level throttling.\n\u003c/div\u003e\n\nEmulating a Slow 3G network is demonstrated below.\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n\n  const client = await page.target().createCDPSession();\n  await client.send('Network.enable');\n  // Simulated network throttling (Slow 3G)\n  await page.emulateNetworkConditions(puppeteer.networkConditions['Slow 3G']);\n  await browser.close();\n})();\n```\n\n[Source](throttle-network.js)\n\nYou can find details on the presets DevTools supports for Slow and Fast 3G in the [official source](https://github.com/ChromeDevTools/devtools-frontend/blob/80c102878fd97a7a696572054007d40560dcdd21/front_end/sdk/NetworkManager.js#L252-L274). If you are looking for the older presets around Regular 4G, WiFi etc, they are captured in [network throttling in Puppeteer](https://fdalvi.github.io/blog/2018-02-05-puppeteer-network-throttle/).\n\n\u003ch3 id=\"throttle-network-cpu\"\u003eEmulate a slow network and CPU\u003c/h3\u003e\n\nCPU throttling allows you to simulate how a page performs on slower mobile devices. This can be done using Puppeteer’s `page.emulateNetworkConditions` API.\n\n🚨 Real device CPU performance is impacted by many factors that are not trivial to emulate via the Chrome DevTools Protocol / Puppeteer. e.g core count, L1/L2 cache, thermal throttling impacting performance, architecture etc. Simulating CPU performance can be a good guideline, but ideally also verify any numbers you see on a real mobile device.\n\nBuilding on top of Slow 3G network throttling, slow CPU throttling (4x slowdown - close to a median-quality device like the Moto G4), is shown below. \n\u003c/div\u003e\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n\n  const client = await page.target().createCDPSession();\n  await client.send('Network.enable');\n  // Simulated network throttling (Slow 3G)\n  await page.emulateNetworkConditions(puppeteer.networkConditions['Slow 3G']);\n  await page.emulateCPUThrottling(4);\n  await browser.close();\n})();\n```\n[Source](throttle-network-cpu.js)\n\n\u003ch3 id=\"javascript-disabled\"\u003eTest your site renders with JavaScript disabled\u003c/h3\u003e\n\nSituations with intermittant\nconnectivity may mean JS is effectively disabled until it can be loaded. Testing a page with JS disabled allows you to simulate a 'worst case' for this.\n\nPuppeteer API: [setRequestInterception()](https://pptr.dev/#?product=Puppeteer\u0026show=api-pagesetrequestinterceptionvalue)\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  await page.setRequestInterception(true);\n\n  page.on('request', request =\u003e {\n    if (request.resourceType() === 'script') {\n      request.abort();\n    } else {\n      request.continue();\n    }\n  });\n\n  await page.goto('https://reddit.com');\n  await page.screenshot({ path: 'pptr-nojs.png' });\n\n  await browser.close();\n})();\n```\n[Source](javascript-disabled.js)\n\n\u003cimg src=\"/assets/images/Performance-tracing7.png\" alt=\"Reddit rendered with JS disabled\"/\u003e\n\n\u003ch3 id=\"navigation-timing\"\u003eGet Navigation Timing API metrics\u003c/h3\u003e\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  await page.goto('https://pptr.dev');\n  const performanceTiming = JSON.parse(\n    await page.evaluate(() =\u003e JSON.stringify(window.performance.timing))\n  );\n  console.log('performanceTiming', performanceTiming)\n  await browser.close();\n})();\n```\n[Source](navigation-timing.js)\n\n\u003cimg src=\"/assets/images/Performance-tracing4.png\" alt=\"Navigation Timing API timings in iTerm from Puppeteer\"/\u003e\n\n\u003ch3 id=\"first-contentful-paint\"\u003eMeasure First Paint \u0026 First Contentful Paint\u003c/h3\u003e\n\nMetric: [First Contentful Paint - web.dev](https://web.dev/fcp)\n\nFirst Contentful Paint (FCP) metric measures the time from a page starting to load to when any part of the page's content is rendered on the screen. \n\nThe \u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/API/Performance_Timeline\"\u003ePerformance Timeline API\u003c/a\u003e supports client-side latency measurements. \u003ccode\u003eperformance.getEntriesByName\u003c/code\u003e returns recorded performance entries based on the provided name (e.g \"first-paint\") and optionally the performance type.\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  const navigationPromise = page.waitForNavigation();\n  await page.goto('https://pptr.dev');\n  \n  await navigationPromise;\n\n  const firstPaint = JSON.parse(\n    await page.evaluate(() =\u003e\n      JSON.stringify(performance.getEntriesByName('first-paint'))\n    )\n  );\n\n  const firstContentfulPaint = JSON.parse(\n    await page.evaluate(() =\u003e\n      JSON.stringify(performance.getEntriesByName('first-contentful-paint'))\n    )\n  );\n\n  console.log(`First paint: ${firstPaint[0].startTime}`);\n  console.log(`First paint: ${firstContentfulPaint[0].startTime}`);\n\n  await browser.close();\n})();\n```\n[Source](first-contentful-paint.js)\n\n\u003ch3 id=\"largest-contentful-paint\"\u003eMeasure Largest Contentful Paint (LCP) w/PerformanceObserver\u003c/h3\u003e\n\nMetric: [Largest Contentful Paint - web.dev](https://web.dev/lcp)\n\nThe Largest Contentful Paint (LCP) metric reports render time for the largest content element visible in the viewport.\n\n🚨 Lighthouse 6.0 onwards supports measuring LCP and CLS in the lab using the approach in \u003ca href=\"#lighthouse-metrics\"\u003eLighthouse metrics\u003c/a\u003e covered earlier. PerformanceObserver is typically used to measure these metrics in the field.\n\n\u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver\"\u003e\u003ccode\u003ePerformanceObserver\u003c/code\u003e\u003c/a\u003e allows you to observe performance measurement events and get notified of new performance entries as they are recorded in the browser's performance timeline. When measuring modern metrics like LCP or CLS with \u003ccode\u003ePerformanceObserver\u003c/code\u003e, you probably want to wait until the page's \u003ca href=\"https://developers.google.com/web/updates/2018/07/page-lifecycle-api\"\u003elifecycle state\u003c/a\u003e has changed to hidden. This ensures that you log the most latest entry.\n\n[Full Puppeteer snippet](https://gist.github.com/addyosmani/c053f68aead473d7585b45c9e8dce31e)\n\n```js\nconst puppeteer = require('puppeteer');\nconst devices = require('puppeteer/DeviceDescriptors');\n\nconst phone = devices.devicesMap['Nexus 5X'];\n\nfunction calcLCP() {\n  window.largestContentfulPaint = 0;\n\n  const observer = new PerformanceObserver((entryList) =\u003e {\n    const entries = entryList.getEntries();\n    const lastEntry = entries[entries.length - 1];\n    window.largestContentfulPaint = lastEntry.renderTime || lastEntry.loadTime;\n  });\n\n  observer.observe({ type: 'largest-contentful-paint', buffered: true });\n\n  document.addEventListener('visibilitychange', () =\u003e {\n    if (document.visibilityState === 'hidden') {\n      observer.takeRecords();\n      observer.disconnect();\n      console.log('LCP:', window.largestContentfulPaint);\n    }\n  });\n}\n\n\nasync function getLCP(url) {\n  const browser = await puppeteer.launch({\n    args: ['--no-sandbox'],\n    timeout: 10000\n  });\n\n  try {\n    const page = await browser.newPage();\n    const client = await page.target().createCDPSession();\n\n    await client.send('Network.enable');\n    await client.send('ServiceWorker.enable');\n    await page.emulateNetworkConditions(puppeteer.networkConditions['Good 3G']);\n    await page.emulateCPUThrottling(4);\n    await page.emulate(phone);\n\n    await page.evaluateOnNewDocument(calcLCP);\n    await page.goto(url, { waitUntil: 'load', timeout: 60000 });\n\n    const lcp = await page.evaluate(() =\u003e {\n      return window.largestContentfulPaint;\n    });\n    browser.close();\n    return lcp;\n  } catch (error) {\n    console.log(error);\n    browser.close();\n  }\n}\n\ngetLCP(\"https://pptr.dev\").then(lcp =\u003e console.log(\"LCP is: \" + lcp));\n```\n[Source](largest-contentful-paint.js)\n\n\u003cimg src=\"/assets/images/Performance-tracing9.png\" alt=\"Largest Contentful Paint\"/\u003e\n\n\n\u003ch3 id=\"cumulative-layout-shift\"\u003eMeasure Cumulative Layout Shift (CLS) w/PerformanceObserver\u003c/h3\u003e\n\nMetric: [Cumulative Layout Shift - web.dev](https://web.dev/cls)\n\nThe Cumulative Layout Shift (CLS) metric measures the sum of individual layout shift scores for each unexpected layout shift that occurs between when the page begins loading and when its lifecycle state changes to hidden.\n\n🚨 Lighthouse 6.0 onwards supports measuring CLS and LCP in the lab using the approach in \u003ca href=\"#lighthouse-metrics\"\u003eLighthouse metrics\u003c/a\u003e covered earlier. PerformanceObserver is typically used to measure these metrics in the field.\n\n\u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver\"\u003e\u003ccode\u003ePerformanceObserver\u003c/code\u003e\u003c/a\u003e allows you to observe performance measurement events and get notified of new performance entries as they are recorded in the browser's performance timeline. When measuring modern metrics like CLS or LCP with \u003ccode\u003ePerformanceObserver\u003c/code\u003e, you probably want to wait until the page's \u003ca href=\"https://developers.google.com/web/updates/2018/07/page-lifecycle-api\"\u003elifecycle state\u003c/a\u003e has changed to hidden. This ensures that you log the most latest entry.\n\n[Full Puppeteer snippet](https://gist.github.com/martinschierle/0b43f3a56da39aa5aa8f8f9dc431f903).\n\n```js\nconst puppeteer = require('puppeteer');\nconst devices = require('puppeteer/DeviceDescriptors');\n  \nconst phone = devices.devicesMap['Nexus 5X'];\n\nfunction calcJank() {\n  window.cumulativeLayoutShiftScore = 0;\n\n  const observer = new PerformanceObserver((list) =\u003e {\n   for (const entry of list.getEntries()) {\n     if (!entry.hadRecentInput) {\n       console.log(\"New observer entry for cls: \" + entry.value);\n       window.cumulativeLayoutShiftScore += entry.value;\n     }\n   }\n  });\n\n  observer.observe({type: 'layout-shift', buffered: true});\n\n  document.addEventListener('visibilitychange', () =\u003e {\n    if (document.visibilityState === 'hidden') {\n      observer.takeRecords();\n      observer.disconnect();\n      console.log('CLS:', window.cumulativeLayoutShiftScore);\n    }\n  });\n}\n\n\nasync function getCLS(url) {\n  const browser = await puppeteer.launch({ \n    args: ['--no-sandbox'],\n    timeout: 10000\n  });\n\n  try {\n    const page = await browser.newPage();\n    const client = await page.target().createCDPSession();\n\n    await client.send('Network.enable');\n    await client.send('ServiceWorker.enable');\n    await page.emulateNetworkConditions(puppeteer.networkConditions['Good 3G']);\n    await page.emulateCPUThrottling(4);\n    await page.emulate(phone);\n    // inject a function with the code from \n    // https://web.dev/cls/#measure-cls-in-javascript\n    await page.evaluateOnNewDocument(calcJank);  \n    await page.goto(url, { waitUntil: 'load', timeout: 60000});\n\n    const cls = await page.evaluate(() =\u003e { \n        return window.cumulativeLayoutShiftScore;\n    });\n    browser.close();\n    return cls;\n  } catch (error) {\n    console.log(error);\n    browser.close();\n  }\n}\n\ngetCLS(\"https://pptr.dev\").then(cls =\u003e console.log(\"CLS is: \" + cls));\n```\n[Source](cumulative-layout-shift.js)\n\n\u003cimg src=\"/assets/images/Performance-tracing10.png\" alt=\"Cumulative Layout Shift\"/\u003e\n\n\u003ch3 id=\"nextjs-metrics\"\u003eMeasure SPA metrics with Next.js\u003c/h3\u003e\n\nThe \u003ca href=\"https://web.dev/custom-metrics/\"\u003eUser Timing API\u003c/a\u003e is a general purpose measurement API for time-based metrics. It allows you to arbitrarily mark points in time and then later measure the duration between those marks using \u003ccode\u003eperformance.mark()\u003c/code\u003e and \u003ccode\u003eperformance.measure\u003c/code\u003e.\n\nOutside of the performance metrics made available via the Navigation Timing API, single-page apps (SPA) often also have custom metrics for tracking other key moments. In Next.js, these could correspond to Time-to-Hydrate, SPA route transitions and so on.\n\nNext.js recently added the [`unstable_onPerformanceData`](https://github.com/zeit/next.js/pull/8480) helper for tracking client-side performance metrics using `performance.mark` and `performance.measure`. The below Puppeteer script allows us to collect this performance data once you patch your app to log key events to localStorage as in this [Glitch example](https://puppeteersandbox.com/w7u0RLhx) by Houssein Djirdeh.\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch()\n  const page = await browser.newPage()\n  let selector = '';\n  page.on('load', () =\u003e console.log(\"Loaded: \" + page.url()));\n  page.on('framenavigated', frame =\u003e {\n    console.log(`new url: ${frame.url()}`);\n  });\n\n  const navigationPromise = page.waitForNavigation({\n    waitUntil: 'networkidle2'\n  })\n\n  // Navigate to random Next.js page\n  await page.goto('https://new-app-3-op9eiblak.now.sh/')\n\n  console.log('\\n==== localStorage hydration entry ====\\n');\n  const hydrationData = await page.evaluate(() =\u003e {\n    const data = {\n      'before-hydrate-mark': localStorage.getItem('beforeRender'),\n      'after-hydrate-mark': Number(localStorage.getItem('beforeRender')) + Number(localStorage.getItem('Next.js-hydration')),\n      'hydration-duration': localStorage.getItem('Next.js-hydration'),\n    };\n    return data;\n  });\n\n  console.log(hydrationData);\n\n  await page.screenshot({\n    path: 'home-page.png',\n    fullPage: true\n  });\n\n  await navigationPromise;\n\n  // Navigate to the Blog\n  selector = '#__next \u003e div \u003e nav \u003e ul \u003e li:nth-child(1) \u003e a';\n  await Promise.all([\n    await page.waitForSelector(selector),\n    await page.click(selector, {\n      delay: 300\n    }),\n    await page.waitFor(4000),\n    await navigationPromise\n  ]);\n\n  console.log('\\n==== localStorage route change performance entries ====\\n');\n  const routeChangeData = await page.evaluate(() =\u003e {\n    const data = {\n      'link-click-to-render-start-duration': localStorage.getItem('Next.js-route-change-to-render'),\n      'render-duration': localStorage.getItem('Next.js-render')\n    };\n    return data;\n  });\n\n  console.log(routeChangeData);\n\n  await page.screenshot({\n    path: 'blog-page.png',\n    fullPage: true\n  });\n\n  await browser.close();\n})();\n```\n[Source](nextjs-metrics.js)\n\n\u003ch3 id=\"devtools-frame-rate\"\u003eGet DevTools-specific metrics: Frames Per Second\u003c/h3\u003e\n\nIt's possible to open a remote debugging client and turn on DevTools-specific features, such as the frames-per-second (FPS) heads-up-display.\n\nPuppeteer API: [createCDPSession()](https://pptr.dev/#?product=Puppeteer\u0026show=api-targetcreatecdpsession)\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const args = await puppeteer.defaultArgs().filter(flag =\u003e flag !== '--enable-automation');\n  const browser = await puppeteer.launch({\n    headless: false,\n    devtools: true,\n    ignoreDefaultArgs: true,\n    args\n  });\n  const page = await browser.newPage();\n  const devtoolsProtocolClient = await page.target().createCDPSession();\n  await devtoolsProtocolClient.send('Overlay.setShowFPSCounter', { show: true });\n  await page.goto('https://pptr.dev');\n  await page.screenshot({ path: './image.jpg', type: 'jpeg' });\n  await page.close();\n  await browser.close();\n})();\n```\n[Source](devtools-frame-rate.js)\n\n\u003cimg src=\"/assets/images/Performance-tracing6a.png\" alt=\"FPS meter from DevTools rendered via Puppeteer\"/\u003e\n\n\u003ch3 id=\"measure-memory-leaks\"\u003eMeasure memory leaks\u003c/h3\u003e\n\nChecking the number of objects retained on the heap can be a good basic start to measuring memory leaks in JavaScript. In Puppeteer, `queryObjects()` can be used to count all the objects with the same prototype somewhere in the prototype chain.\n\nPuppeteer API: [queryObjects()](https://pptr.dev/#?product=Puppeteer\u0026version=v2.1.1\u0026show=api-pagequeryobjectsprototypehandle)\n\nFor a more detailed look at this topic, check out [automatically detecting memory-leaks with Puppeteer](https://media-codings.com/articles/automatically-detect-memory-leaks-with-puppeteer).\n\n```js\nconst puppeteer = require('puppeteer');\n\n// Helper by @chrisguttandin\nconst countObjects = async (page) =\u003e {\n  const prototypeHandle = await page.evaluateHandle(() =\u003e Object.prototype);\n  const objectsHandle = await page.queryObjects(prototypeHandle);\n  const numberOfObjects = await page.evaluate((instances) =\u003e instances.length, objectsHandle);\n\n  await Promise.all([\n    prototypeHandle.dispose(),\n    objectsHandle.dispose()\n  ]);\n\n  return numberOfObjects;\n};\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.createIncognitoBrowserContext();\n\n  const numberOfObjects = await countObjects(page);\n  console.log(numberOfObjects);\n\n  await page.evaluate(() =\u003e {\n    class SomeObject {\n      constructor () {\n        this.numbers = {}\n        for (let i = 0; i \u003c 1000; i++) {\n          this.numbers[Math.random()] = Math.random()\n        }\n      }\n    }\n    const someObject = new SomeObject();\n    const onMessage = () =\u003e { /* ... */ };\n    window.addEventListener('message', onMessage);\n  });\n\n  const numberOfObjectsAfter = await countObjects(page);\n  console.log(numberOfObjectsAfter);\n\n  // Check if the number of retained objects is expected\n  // expect(await countObjects(page)).to.equal(0);\n\n  await browser.close();\n})();\n```\n[Source](measure-memory-leaks.js)\n\n\u003ch3 id=\"request-interception\"\u003eOverride requests with Request Interception\u003c/h3\u003e\n\nRequest interception (overrides) allows you to modify network requests that are made by a page. \n\nPuppeteer API: [setRequestInterception()](https://pptr.dev/#?product=Puppeteer\u0026version=v3.0.0\u0026show=api-pagesetrequestinterceptionvalue)\n\n\u003ch4\u003eBlock requests for images\u003c/h4\u003e\n\n```js\nimport puppeteer from 'puppeteer';\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n\n  await page.setRequestInterception(true);\n  \n  page.on('request', (req) =\u003e {\n    if (req.resourceType() === 'image'){\n      req.abort();\n    } else {\n      req.continue();\n    }\n  });\n\n  await page.goto('https://bbc.com');\n  await page.screenshot({path: 'no-images.png', fullPage: true});\n  await browser.close();\n})();\n```\n[Source](request-interception-block-images.js)\n\n\u003ch4\u003eReplace a remote resource with a local one\u003c/h4\u003e\n\nIn the below snippet, we override a remote resource for the Puppeteer site (pptr.dev/style.css) with a local version (assets/style.css). In the version served, you'll see the Puppeteer site is rendered with our green background colors instead.\n\n```js\nconst puppeteer = require('puppeteer');\nconst fs = require('fs');\nconst path = require('path');\n\n(async () =\u003e {\n\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  // URL to test\n  const remoteURL = 'https://pptr.dev/index.html';\n  // URL to replace\n  const remoteFilePath = 'https://pptr.dev/style.css';\n  // Local (override) file to use instead\n  const localFilePath = path.join(__dirname, \"./assets/style.css\");\n\n  await page.setRequestInterception(true);\n\n  page.on('request', interceptedRequest =\u003e {\n    const url = interceptedRequest.url();\n    console.log(`Intercepted ${url}`);\n\n    if (url === remoteFilePath \u0026\u0026 !url.match(localFilePath)) {\n      interceptedRequest.respond({\n        body: fs.readFileSync(\n          localFilePath\n        )\n      });\n    } else {\n      interceptedRequest.continue();\n    }\n  });\n\n  await page.goto(remoteURL, {\n    waitUntil: 'networkidle2'\n  });\n\n  await page.screenshot({path: 'override.png', fullPage: true});\n  await browser.close();\n\n})();\n```\n[Source](request-interception-replace-resources.js)\n\n![Puppeteer comparison before and after request interception](/assets/images/pptr-comparison.jpg)\n\nWhile this example doesn't demonstrate it, you could use network overrides to experiment with the before/after for a number of different performance optimizations.\n\n\n\u003ch3 id=\"block-third-parties\"\u003eBlock third-party domains\u003c/h3\u003e\n\nYou can block specific requests using Puppeteer's request interception feature. This can be helpful for profiling performance with specific domains blocked to see the before/after.\n\nPuppeteer API: [setRequestInterception](https://pptr.dev/#?product=Puppeteer\u0026version=v2.1.1\u0026show=api-pagesetrequestinterceptionvalue)\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async() =\u003e {\n  const browser = await puppeteer.launch({\n    headless: true\n  });\n  const page = await browser.newPage();\n  const options = {\n    waitUntil: 'networkidle2',\n    timeout: 30000\n  };\n\n  // Before: Normal navigtation\n  await page.goto('https://theverge.com', options);\n  await page.screenshot({path: 'before.png', fullPage: true});\n  const metrics = await page.metrics();\n  console.info(metrics);\n\n  // After: Navigation with some domains blocked\n  \n  // Array of third-party domains to block\n  const blockedDomains = [\n    'https://pagead2.googlesyndication.com',\n    'https://creativecdn.com',\n    'https://www.googletagmanager.com',\n    'https://cdn.krxd.net',\n    'https://adservice.google.com',\n    'https://cdn.concert.io',\n    'https://z.moatads.com',\n    'https://cdn.permutive.com'];\n  await page.setRequestInterception(true);\n  page.on('request', request =\u003e {\n    const url = request.url()\n    if (blockedDomains.some(d =\u003e url.startsWith(d))) {\n      request.abort();\n    } else {\n      request.continue();\n    }\n  });\n  \n  await page.goto('https://theverge.com', options);\n  await page.screenshot({path: 'after.png', fullPage: true});\n\n  const metricsAfter = await page.metrics();\n  console.info(metricsAfter);\n\n  await browser.close();\n})();\n```\n[Source](request-interception-block-third-parties.js)\n\n![The Verge before/after domain blocking](/assets/images/comparison-before-after@2x.jpg)\n\n\u003ch3 id=\"code-coverage\"\u003eCode Coverage for JavaScript and CSS\u003c/h3\u003e\n\nPuppeteer API: [page.coverage.startJSCoverage()](https://pptr.dev/#?product=Puppeteer\u0026version=v3.0.1\u0026show=api-coveragestartjscoverageoptions)\n\n```js\nconst puppeteer = require('puppeteer');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n\n  // Gather coverage for JS and CSS files\n  await Promise.all([page.coverage.startJSCoverage(), page.coverage.startCSSCoverage()]);\n\n  await page.goto('https://pptr.dev');\n\n  // Stops the coverage gathering\n  const [jsCoverage, cssCoverage] = await Promise.all([\n    page.coverage.stopJSCoverage(),\n    page.coverage.stopCSSCoverage(),\n  ]);\n\n  // Calculates # bytes being used based on the coverage\n  const calculateUsedBytes = (type, coverage) =\u003e\n    coverage.map(({url, ranges, text}) =\u003e {\n      let usedBytes = 0;\n\n      ranges.forEach((range) =\u003e (usedBytes += range.end - range.start - 1));\n\n      return {\n        url,\n        type,\n        usedBytes,\n        totalBytes: text.length,\n        percentUsed: `${(usedBytes / text.length * 100).toFixed(2)}%`\n      };\n    });\n\n  console.info([\n    ...calculateUsedBytes('js', jsCoverage),\n    ...calculateUsedBytes('css', cssCoverage),\n  ]);\n\n  await browser.close();\n})();\n```\n\n[Source](code-coverage.js)\n\nOutput preview:\n\n```sh\n  {\n    url: 'https://pptr.dev/index.js',\n    type: 'js',\n    usedBytes: 59370,\n    totalBytes: 141703,\n    percentUsed: '41.90%'\n  },\n  {\n    url: 'https://www.googletagmanager.com/gtag/js?id=UA-106086244-2',\n    type: 'js',\n    usedBytes: 20646,\n    totalBytes: 81644,\n    percentUsed: '25.29%'\n  },\n  {\n    url: 'https://pptr.dev/style.css',\n    type: 'css',\n    usedBytes: 1409,\n    totalBytes: 14326,\n    percentUsed: '9.84%'\n  }\n```\n\n\u003ch3 id=\"har-file\"\u003eSave network requests to a HAR file\u003c/h3\u003e\n\n💡 A [HAR](https://www.keycdn.com/support/what-is-a-har-file) (HTTP Archive) file is a JSON format for tracking performance issues. It keeps track of each resource loaded over the network by the browser including the timing information for each of these resources.\n\nYou can use the [puppeteer-har](https://github.com/Everettss/puppeteer-har) package to generate a HAR file as follows:\n\n```js\nconst puppeteer = require('puppeteer');\nconst HAR = require('puppeteer-har');\n\n(async () =\u003e {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n\n  const har = new HAR(page);\n  await har.start({ path: 'results.har' });\n  await page.goto('https://pptr.dev');\n  await har.stop();\n  await browser.close();\n})();\n```\n\n[Source](generate-har.js)\n\nHAR files can be imported back into Chrome DevTools for analysis or alternatively can be viewed in the [HAR Analyzer](https://toolbox.googleapps.com/apps/har_analyzer/) project.\n\n![Generated HAR file being loaded into the Network panel in DevTools](/assets/images/puppeteer-har.png)\n\n\n## Read more\n\n* [Getting to know Puppeteer using practical examples](https://nitayneeman.com/posts/getting-to-know-puppeteer-using-practical-examples/)\n* [Performance testing in the browser](https://github.com/llatinov/sample-performance-testing-in-browser)\n* [Getting to Know Puppeteer Using Practical Examples](https://nitayneeman.com/posts/getting-to-know-puppeteer-using-practical-examples/)\n* [End to end testing React apps with Puppeteer](https://blog.logrocket.com/end-to-end-testing-react-apps-with-puppeteer-and-jest-ce2f414b4fd7/)\n* [using CDP with Puppeteer](https://medium.com/@jsoverson/using-chrome-devtools-protocol-with-puppeteer-737a1300bac0)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faddyosmani%2Fpuppeteer-webperf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faddyosmani%2Fpuppeteer-webperf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faddyosmani%2Fpuppeteer-webperf/lists"}