I'm a Technical SEO Director with a passion for building tools and optimising web performance. I specialise in creating user-first strategies that leverage data to enhance SEO and deliver exceptional website experiences. I thrive on simplifying complex challenges to help websites attract more visitors, improve performance, and keep users engaged.
Site performance plays a crucial role in delivering a seamless user experience. Slow load times, unexpected layout shifts, and unresponsive interactions can negatively impact both SEO and user satisfaction. However, identifying these issues under real-world conditions requires the right tools.
This is where Puppeteer comes in.
Puppeteer is a Node.js library that allows developers and technical SEOs to automate headless Chrome browsers. A headless browser refers to a web browser that runs without the usual interface. In this case, the browser is run through your command line.
It enables you to simulate real user environments—including different network speeds, devices, and CPU capabilities—and measure key performance metrics such as Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and Interaction to Next Paint (INP).
In this guide, I’ll break down the key concepts behind performance testing and walk you step-by-step through building your site speed testing tool using Puppeteer. By the end, you’ll be able to simulate real-world scenarios, gather actionable performance insights, and export your results for reporting.
My goal is to empower you to go beyond the standard toolbox—to experiment with different environments, uncover hidden bottlenecks, and gain a deeper understanding of how your website truly performs under various conditions. Addy Osmani has compiled an extensive list of Puppeteer recipes for performance testing here, that can help expand further what I will go over in this article to help you build the best possible tool.
By building this tool, you’ll not only learn how to replicate real-world user experiences but also develop the flexibility to test, measure, and optimise website performance in a way that works for you.
Setting Up Puppeteer
Installing Puppeteer
To get started, ensure you have Node.js and NPM installed. If not, download them from the Node.js website. Once ready, install Puppeteer by running:
npm i puppeteer
Puppeteer comes with its own Chromium version, ensuring compatibility with its API.
Test the Setup with a Basic Script
To confirm everything is working, here’s a script that opens a headless browser, navigates to a page, and logs basic metrics:
const puppeteer = require("puppeteer"); async function runBasicTest(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); const metrics = await page.metrics(); console.log("Basic Metrics:", metrics); await browser.close(); } runBasicTest("https://www.example.com");
page.metrics() returns a promise that resolves to an object containing key runtime metrics, which are useful for analysing a site’s performance.
Output Example:
Basic Metrics: { Timestamp: 1234567890, Nodes: 2045, Frames: 1, ScriptDuration: 1.2, LayoutDuration: 0.45 }
This confirms Puppeteer is ready to go.
Simulating Real-World Network Conditions
Users access your site on various network connections, from blazing-fast Wi-Fi to painfully slow 3G. Network speed affects key metrics like Largest Contentful Paint (LCP), which directly impacts user experience.
Using Predefined Network Conditions
Puppeteer provides ready-to-use network profiles with the ‘Page.emulateNetworkConditions() method’:
import { PredefinedNetworkConditions } from "puppeteer"; const slow3G = PredefinedNetworkConditions["Slow 3G"]; const fast4G = PredefinedNetworkConditions["Fast 4G"]; await page.emulateNetworkConditions(slow3G);
Predefined Profiles:
- Slow 3G: 400ms latency, 500 Kbps download.
- Fast 4G: 20ms latency, 10 Mbps download.
Defining Custom Network Conditions
For more precise control, create custom profiles:
const customNetwork = { offline: false, downloadThroughput: 1 * 1024 * 1024 / 8, // 1 Mbps uploadThroughput: 512 * 1024 / 8, // 512 Kbps latency: 500 // 500ms latency }; await page.emulateNetworkConditions(customNetwork);
Custom conditions let you simulate networks, rural connections, or edge cases specific to your audience.
Emulating Devices
Performance varies dramatically across devices. High-end smartphones like the iPhone 13 perform better than older Android devices. Testing across devices ensures you optimise for all users.
Using Device Descriptors
Puppeteer’s built-in device descriptors emulate real-world devices with a simple function emulate():
const iPhoneX = puppeteer.devices["iPhone X"]; await page.emulate(iPhoneX);
Descriptors include:
- Screen size
- Device pixel ratio
- User agent
The list of devices available can be found in the puppeteer device descriptors. The device can be referenced utilising the ‘Name’ parameter.
To make things easier when running multiple tests, we recommend grabbing a list of the most important devices your users use. With a more advanced script, you could loop through each device and generate a report analysing how performance compares with each device.
Looping Through Popular Devices
Use analytics data to identify the most common devices your audience uses and test them:
const devicesToTest = [ puppeteer.devices["iPhone 11 Pro"], puppeteer.devices["Galaxy S9+"], puppeteer.devices["Pixel 5"] ]; for (const device of devicesToTest) { await page.emulate(device); console.log(`Testing on: ${device.name}`); }
Simulating CPU Throttling
To replicate low-end devices or multitasking scenarios, you can throttle the CPU. Throttling ensures your tests reflect real-world conditions, as development machines often outperform user hardware.
await page.emulateCPUThrottling(4);
A 4x throttle reduces CPU speed to 25%, mimicking older smartphones.
Collecting Core Web Vitals
The Core Web Vitals—LCP, CLS, and INP—are critical for understanding real user experience. Using PerformanceObserver, Puppeteer can programmatically measure these metrics.
Performance observer interface is utilised to observe performance measure events and notify us of performance entries as they are recorded in the browser’s performance timeline. For this case, we will use the performance entry list to get the metrics we want. This documentation is a full list of anything you might need to build your tool.
Measuring LCP
const lcpObserver = new PerformanceObserver((list) => { const lastEntry = list.getEntries().pop(); console.log("LCP:", lastEntry.renderTime || lastEntry.loadTime); }); lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
Measuring CLS
const clsObserver = new PerformanceObserver((list) => { let clsValue = 0; list.getEntries().forEach((entry) => { if (!entry.hadRecentInput) clsValue += entry.value; }); console.log("CLS:", clsValue); }); clsObserver.observe({ type: "layout-shift", buffered: true });
Measuring INP
const inpobserver = new PerformanceObserver((list) => { console.log(list.getEntries()); }); inpobserver.observe({ type: "long-animation-frame", buffered: true });
Combining It All
Here’s the complete script to simulate real-world conditions and measure Core Web Vitals:
const puppeteer = require("puppeteer"); async function runBrowserTest(url) { const browser = await puppeteer.launch({ headless: true, // Headless mode for performance defaultViewport: null, }); const page = await browser.newPage(); // Navigate to the URL and inject PerformanceObserver await page.goto(url); const metrics = await page.evaluate(() => { // Object to store CWV metrics const metricsData = { LCP: null, CLS: 0, INP: [], // Store individual durations for INP }; // LCP const lcpObserver = new PerformanceObserver((entryList) => { const entries = entryList.getEntries(); const lastEntry = entries[entries.length - 1]; metricsData.LCP = lastEntry.renderTime || lastEntry.loadTime; }); lcpObserver.observe({ type: "largest-contentful-paint", buffered: true }); // CLS const clsObserver = new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { if (!entry.hadRecentInput) { metricsData.CLS += entry.value; } } }); clsObserver.observe({ type: "layout-shift", buffered: true }); // INP const inpObserver = new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { metricsData.INP.push(entry.duration); console.log(`New INP Duration: ${entry.duration}ms`); } }); inpObserver.observe({ type: "long-animation-frame", buffered: true }); return new Promise((resolve) => { setTimeout(() => { resolve(metricsData); }, 5000); // 5-second delay for CWV calculation }); }); console.log("Core Web Vitals Metrics:", metrics); await browser.close(); } runBrowserTest("https://example.com");
Exporting Results with xlsx
To make results shareable, export them into an Excel file using the xlsx
library:
Install xlsx
npm install xlsx
Exporting Data
const XLSX = require("xlsx"); const performanceData = [ { URL: "https://example.com", Device: "iPhone X", Network: "Slow 3G", LCP: 2400, CLS: 0.12, INP: 200 } ]; const worksheet = XLSX.utils.json_to_sheet(performanceData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "Performance Results"); XLSX.writeFile(workbook, "performance_results.xlsx"); console.log("Results exported to performance_results.xlsx");
Conclusion
By following this guide, you’ve built a powerful site speed-testing tool with Puppeteer. It allows you to:
- Simulate real-world network conditions, devices, and CPU speeds.
- Measure key metrics like LCP, CLS, and INP.
- Export results into Excel for easy reporting.
Use this tool to identify performance bottlenecks, prioritise fixes, and continually improve your users’ experience.
With Puppeteer’s flexibility, the possibilities for enhancement—like automating batch tests or visualising results—are endless.
[…] Продуктивність • Виявлення регресій продуктивності вебу за допомогою статистичних інструментів • Ледаче завантаження проти… Eager loading](https://blog.logrocket.com/lazy-loading-vs-eager-loading/) • Створіть власний інструмент для тестування швидкості … […]