├── .eslintignore ├── .gitignore ├── README.md ├── package.json ├── puppeteer-devtools-protocol.js ├── puppeteer-performance-timing.js ├── puppeteer-performance-metrics.js ├── puppeteer-lighthouse.js ├── helpers.js ├── .eslintrc.js └── config.performance.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | npm-debug.log 3 | results/* 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sample-performance-testing-in-browser 2 | 3 | Code examples for following post: 4 | 5 | * Performance testing in the browser 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-performance-testing-in-browser", 3 | "version": "1.0.0", 4 | "description": "Performance testing in browser with Puppeteer and Lighthouse", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "eslint .", 9 | "lighthouse": "lighthouse" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "eslint": "^4.19.1", 15 | "lighthouse": "^2.9.4", 16 | "puppeteer": "^1.4.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /puppeteer-devtools-protocol.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const puppeteer = require('puppeteer'); 4 | const throughputKBs = process.env.throughput || 200; 5 | 6 | (async () => { 7 | const browser = await puppeteer.launch({ 8 | executablePath: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 9 | headless: false 10 | }); 11 | const page = await browser.newPage(); 12 | const client = await page.target().createCDPSession(); 13 | 14 | await client.send('Network.emulateNetworkConditions', { 15 | offline: false, 16 | latency: 200, 17 | downloadThroughput: throughputKBs * 1024, 18 | uploadThroughput: throughputKBs * 1024 19 | }); 20 | 21 | const start = (new Date()).getTime(); 22 | await client.send('Page.navigate', { 23 | 'url': 'https://automationrhapsody.com' 24 | }); 25 | await page.waitForNavigation({ 26 | timeout: 240000, 27 | waitUntil: 'load' 28 | }); 29 | const end = (new Date()).getTime(); 30 | const totalTimeSeconds = (end - start) / 1000; 31 | 32 | console.log(`Page loaded for ${totalTimeSeconds} seconds when connection is ${throughputKBs}Kbit/s`); 33 | 34 | await browser.close(); 35 | })(); -------------------------------------------------------------------------------- /puppeteer-performance-timing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const puppeteer = require('puppeteer'); 4 | const { gatherPerformanceTimingMetric, 5 | gatherPerformanceTimingMetrics, 6 | processPerformanceTimingMetrics } = require('./helpers'); 7 | 8 | (async () => { 9 | const browser = await puppeteer.launch({ 10 | headless: true 11 | }); 12 | const page = await browser.newPage(); 13 | await page.goto('https://automationrhapsody.com/'); 14 | 15 | const rawMetrics = await gatherPerformanceTimingMetrics(page); 16 | const metrics = await processPerformanceTimingMetrics(rawMetrics); 17 | console.log(`DNS: ${metrics.dnsLookup}`); 18 | console.log(`TCP: ${metrics.tcpConnect}`); 19 | console.log(`Req: ${metrics.request}`); 20 | console.log(`Res: ${metrics.response}`); 21 | console.log(`DOM load: ${metrics.domLoaded}`); 22 | console.log(`DOM interactive: ${metrics.domInteractive}`); 23 | console.log(`Document load: ${metrics.pageLoad}`); 24 | console.log(`Full load time: ${metrics.fullTime}`); 25 | 26 | const loadEventEnd = await gatherPerformanceTimingMetric(page, 'loadEventEnd'); 27 | const date = new Date(loadEventEnd); 28 | console.log(`Page load ended on: ${date}`); 29 | 30 | await browser.close(); 31 | })(); 32 | 33 | -------------------------------------------------------------------------------- /puppeteer-performance-metrics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const puppeteer = require('puppeteer'); 4 | const perfConfig = require('./config.performance.js'); 5 | const { gatherPerformanceTimingMetrics, 6 | gatherLighthouseMetrics } = require('./helpers'); 7 | 8 | (async () => { 9 | const browser = await puppeteer.launch({ 10 | headless: true 11 | }); 12 | const page = await browser.newPage(); 13 | const urls = ['https://automationrhapsody.com/', 14 | 'https://automationrhapsody.com/examples/sample-login/']; 15 | 16 | for (const url of urls) { 17 | await page.goto(url); 18 | 19 | const lighthouseMetrics = await gatherLighthouseMetrics(page, perfConfig); 20 | const firstPaint = parseInt(lighthouseMetrics.audits['first-meaningful-paint']['rawValue'], 10); 21 | const firstInteractive = parseInt(lighthouseMetrics.audits['first-interactive']['rawValue'], 10); 22 | const navigationMetrics = await gatherPerformanceTimingMetrics(page); 23 | const domInteractive = navigationMetrics.domInteractive - navigationMetrics.navigationStart; 24 | const fullLoad = navigationMetrics.loadEventEnd - navigationMetrics.navigationStart; 25 | console.log(`FirstPaint: ${firstPaint}, FirstInterractive: ${firstInteractive}, DOMInteractive: ${domInteractive}, FullLoad: ${fullLoad}`); 26 | } 27 | 28 | await browser.close(); 29 | })(); 30 | 31 | -------------------------------------------------------------------------------- /puppeteer-lighthouse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const puppeteer = require('puppeteer'); 4 | const perfConfig = require('./config.performance.js'); 5 | const fs = require('fs'); 6 | const resultsDir = 'results'; 7 | const { gatherLighthouseMetrics } = require('./helpers'); 8 | 9 | (async () => { 10 | const browser = await puppeteer.launch({ 11 | headless: true, 12 | // slowMo: 250 13 | }); 14 | const page = await browser.newPage(); 15 | 16 | await page.goto('https://automationrhapsody.com/examples/sample-login/'); 17 | await verify(page, 'page_home'); 18 | 19 | await page.click('a'); 20 | await page.waitForSelector('form'); 21 | await page.type('input[name="username"]', 'admin'); 22 | await page.type('input[name="password"]', 'admin'); 23 | await page.click('input[type="submit"]'); 24 | await page.waitForSelector('h2'); 25 | await verify(page, 'page_loggedin'); 26 | 27 | await browser.close(); 28 | })(); 29 | 30 | async function verify(page, pageName) { 31 | await createDir(resultsDir); 32 | await page.screenshot({ path: `./${resultsDir}/${pageName}.png`, fullPage: true }); 33 | const metrics = await gatherLighthouseMetrics(page, perfConfig); 34 | fs.writeFileSync(`./${resultsDir}/${pageName}.json`, JSON.stringify(metrics, null, 2)); 35 | return metrics; 36 | } 37 | 38 | async function createDir(dirName) { 39 | if (!fs.existsSync(dirName)) { 40 | fs.mkdirSync(dirName, '0766'); 41 | } 42 | } -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const lighthouse = require('lighthouse'); 4 | 5 | async function gatherPerformanceTimingMetric(page, metricName) { 6 | const metric = await page.evaluate(metric => window.performance.timing[metric], metricName); 7 | return metric; 8 | } 9 | 10 | async function gatherPerformanceTimingMetrics(page) { 11 | // The values returned from evaluate() function should be JSON serializeable. 12 | const rawMetrics = await page.evaluate(() => JSON.stringify(window.performance.timing)); 13 | const metrics = JSON.parse(rawMetrics); 14 | return metrics; 15 | } 16 | 17 | async function processPerformanceTimingMetrics(metrics) { 18 | return { 19 | dnsLookup: metrics.domainLookupEnd - metrics.domainLookupStart, 20 | tcpConnect: metrics.connectEnd - metrics.connectStart, 21 | request: metrics.responseStart - metrics.requestStart, 22 | response: metrics.responseEnd - metrics.responseStart, 23 | domLoaded: metrics.domComplete - metrics.domLoading, 24 | domInteractive: metrics.domInteractive - metrics.navigationStart, 25 | pageLoad: metrics.loadEventEnd - metrics.loadEventStart, 26 | fullTime: metrics.loadEventEnd - metrics.navigationStart 27 | } 28 | } 29 | 30 | async function gatherLighthouseMetrics(page, config) { 31 | // Port is in formаt: ws://127.0.0.1:52046/devtools/browser/675a2fad-4ccf-412b-81bb-170fdb2cc39c 32 | const port = await page.browser().wsEndpoint().split(':')[2].split('/')[0]; 33 | return await lighthouse(page.url(), { port: port }, config).then(results => { 34 | delete results.artifacts; 35 | return results; 36 | }); 37 | } 38 | 39 | module.exports = { 40 | gatherPerformanceTimingMetric, 41 | gatherPerformanceTimingMetrics, 42 | processPerformanceTimingMetrics, 43 | gatherLighthouseMetrics 44 | }; -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | 4 | "env": { 5 | "node": true, 6 | "es6": true 7 | }, 8 | 9 | "parserOptions": { 10 | "ecmaVersion": 8 11 | }, 12 | 13 | /** 14 | * ESLint rules 15 | * 16 | * All available rules: http://eslint.org/docs/rules/ 17 | * 18 | * Rules take the following form: 19 | * "rule-name", [severity, { opts }] 20 | * Severity: 2 == error, 1 == warning, 0 == off. 21 | */ 22 | "rules": { 23 | /** 24 | * Enforced rules 25 | */ 26 | 27 | 28 | // syntax preferences 29 | "quotes": [2, "single", { 30 | "avoidEscape": true, 31 | "allowTemplateLiterals": true 32 | }], 33 | "semi": 2, 34 | "no-extra-semi": 2, 35 | "comma-style": [2, "last"], 36 | "wrap-iife": [2, "inside"], 37 | "spaced-comment": [2, "always", { 38 | "markers": ["*"] 39 | }], 40 | "eqeqeq": [2], 41 | "arrow-body-style": [2, "as-needed"], 42 | "accessor-pairs": [2, { 43 | "getWithoutSet": false, 44 | "setWithoutGet": false 45 | }], 46 | "brace-style": [2, "1tbs", { 47 | "allowSingleLine": true 48 | }], 49 | "curly": [2, "multi-line", "consistent"], 50 | "new-parens": 2, 51 | "func-call-spacing": 2, 52 | "arrow-parens": [2, "as-needed"], 53 | "prefer-const": 2, 54 | "quote-props": [2, "consistent"], 55 | 56 | // anti-patterns 57 | "no-var": 2, 58 | "no-with": 2, 59 | "no-multi-str": 2, 60 | "no-caller": 2, 61 | "no-implied-eval": 2, 62 | "no-labels": 2, 63 | "no-new-object": 2, 64 | "no-octal-escape": 2, 65 | "no-self-compare": 2, 66 | "no-shadow-restricted-names": 2, 67 | "no-cond-assign": 2, 68 | "no-debugger": 2, 69 | "no-dupe-keys": 2, 70 | "no-duplicate-case": 2, 71 | "no-empty-character-class": 2, 72 | "no-unreachable": 2, 73 | "no-unsafe-negation": 2, 74 | "radix": 2, 75 | "valid-typeof": 2, 76 | "no-unused-vars": [2, { 77 | "args": "all", 78 | "vars": "all", 79 | "varsIgnorePattern": "([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)" 80 | }], 81 | "no-implicit-globals": [2], 82 | 83 | // es2015 features 84 | "require-yield": 2, 85 | "template-curly-spacing": [2, "never"], 86 | 87 | // spacing details 88 | "space-infix-ops": 2, 89 | "space-in-parens": [2, "never"], 90 | "space-before-function-paren": [2, { 91 | "anonymous": "never", 92 | "named": "never", 93 | "asyncArrow": "always" 94 | }], 95 | "no-whitespace-before-property": 2, 96 | "keyword-spacing": [2, { 97 | "overrides": { 98 | "if": { "after": true }, 99 | "else": { "after": true }, 100 | "for": { "after": true }, 101 | "while": { "after": true }, 102 | "do": { "after": true }, 103 | "switch": { "after": true }, 104 | "return": { "after": true } 105 | } 106 | }], 107 | "arrow-spacing": [2, { 108 | "after": true, 109 | "before": true 110 | }], 111 | 112 | // file whitespace 113 | "no-multiple-empty-lines": [2, { "max": 2 }], 114 | "no-mixed-spaces-and-tabs": 2, 115 | "no-trailing-spaces": 2, 116 | "linebreak-style": [process.platform === "win32" ? 0 : 2, "unix"], 117 | "indent": [2, 2, { 118 | "SwitchCase": 1, 119 | "CallExpression": { 120 | "arguments": 2 121 | }, 122 | "MemberExpression": 2 123 | }], 124 | "key-spacing": [2, { 125 | "beforeColon": false 126 | }] 127 | } 128 | }; -------------------------------------------------------------------------------- /config.performance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | passes: [{ 5 | passName: 'defaultPass', 6 | recordTrace: true, 7 | pauseAfterLoadMs: 5250, 8 | networkQuietThresholdMs: 5250, 9 | cpuQuietThresholdMs: 5250, 10 | useThrottling: true, 11 | gatherers: [ 12 | 'url', 13 | 'scripts', 14 | 'css-usage', 15 | 'viewport', 16 | 'viewport-dimensions', 17 | 'theme-color', 18 | 'manifest', 19 | 'runtime-exceptions', 20 | 'chrome-console-messages', 21 | 'image-usage', 22 | 'accessibility', 23 | 'dobetterweb/all-event-listeners', 24 | 'dobetterweb/anchors-with-no-rel-noopener', 25 | 'dobetterweb/appcache', 26 | 'dobetterweb/domstats', 27 | 'dobetterweb/js-libraries', 28 | 'dobetterweb/optimized-images', 29 | 'dobetterweb/password-inputs-with-prevented-paste', 30 | 'dobetterweb/response-compression', 31 | 'dobetterweb/tags-blocking-first-paint', 32 | 'dobetterweb/websql', 33 | 'fonts', 34 | ], 35 | }], 36 | audits: [ 37 | 'first-meaningful-paint', 38 | 'first-interactive', 39 | 'consistently-interactive', 40 | 'speed-index-metric', 41 | 'estimated-input-latency', 42 | 'time-to-first-byte', 43 | 'redirects', 44 | 'uses-rel-preload', 45 | 'critical-request-chains', 46 | 'network-requests', 47 | 'user-timings', 48 | 'bootup-time', 49 | 'screenshot-thumbnails', 50 | 'mainthread-work-breakdown', 51 | 'font-display', 52 | 'dobetterweb/link-blocking-first-paint', 53 | 'dobetterweb/script-blocking-first-paint', 54 | 'dobetterweb/dom-size', 55 | 'byte-efficiency/uses-responsive-images', 56 | 'byte-efficiency/offscreen-images', 57 | 'byte-efficiency/unminified-css', 58 | 'byte-efficiency/unminified-javascript', 59 | 'byte-efficiency/unused-css-rules', 60 | 'byte-efficiency/uses-optimized-images', 61 | 'byte-efficiency/uses-webp-images', 62 | 'byte-efficiency/uses-request-compression', 63 | 'byte-efficiency/total-byte-weight', 64 | 'byte-efficiency/uses-long-cache-ttl', 65 | ], 66 | groups: { 67 | 'perf-metric': { 68 | title: 'Metrics', 69 | description: 'These metrics encapsulate your web app\'s performance across a number of dimensions.', 70 | }, 71 | 'perf-hint': { 72 | title: 'Opportunities', 73 | description: 'These are opportunities to speed up your application by optimizing the following resources.', 74 | }, 75 | 'perf-info': { 76 | title: 'Diagnostics', 77 | description: 'More information about the performance of your application.', 78 | }, 79 | }, 80 | categories: { 81 | performance: { 82 | name: 'Performance', 83 | description: 'These encapsulate your web app\'s current performance and opportunities to improve it.', 84 | audits: [ 85 | {id: 'first-meaningful-paint', weight: 5, group: 'perf-metric'}, 86 | {id: 'first-interactive', weight: 5, group: 'perf-metric'}, 87 | {id: 'consistently-interactive', weight: 5, group: 'perf-metric'}, 88 | {id: 'speed-index-metric', weight: 1, group: 'perf-metric'}, 89 | {id: 'estimated-input-latency', weight: 1, group: 'perf-metric'}, 90 | {id: 'link-blocking-first-paint', weight: 0, group: 'perf-hint'}, 91 | {id: 'script-blocking-first-paint', weight: 0, group: 'perf-hint'}, 92 | {id: 'uses-responsive-images', weight: 0, group: 'perf-hint'}, 93 | {id: 'offscreen-images', weight: 0, group: 'perf-hint'}, 94 | {id: 'unminified-css', weight: 0, group: 'perf-hint'}, 95 | {id: 'unminified-javascript', weight: 0, group: 'perf-hint'}, 96 | {id: 'unused-css-rules', weight: 0, group: 'perf-hint'}, 97 | {id: 'uses-optimized-images', weight: 0, group: 'perf-hint'}, 98 | {id: 'uses-webp-images', weight: 0, group: 'perf-hint'}, 99 | {id: 'uses-request-compression', weight: 0, group: 'perf-hint'}, 100 | {id: 'time-to-first-byte', weight: 0, group: 'perf-hint'}, 101 | {id: 'redirects', weight: 0, group: 'perf-hint'}, 102 | {id: 'uses-rel-preload', weight: 0, group: 'perf-hint'}, 103 | {id: 'total-byte-weight', weight: 0, group: 'perf-info'}, 104 | {id: 'uses-long-cache-ttl', weight: 0, group: 'perf-info'}, 105 | {id: 'dom-size', weight: 0, group: 'perf-info'}, 106 | {id: 'critical-request-chains', weight: 0, group: 'perf-info'}, 107 | {id: 'network-requests', weight: 0}, 108 | {id: 'user-timings', weight: 0, group: 'perf-info'}, 109 | {id: 'bootup-time', weight: 0, group: 'perf-info'}, 110 | {id: 'screenshot-thumbnails', weight: 0}, 111 | {id: 'mainthread-work-breakdown', weight: 0, group: 'perf-info'}, 112 | {id: 'font-display', weight: 0, group: 'perf-info'}, 113 | ], 114 | }, 115 | }, 116 | }; 117 | --------------------------------------------------------------------------------