Introducing perf
budgets on CI with
Puppeteer
with the help of minions
Önder Ceylan
@onderceylan
Sharing knowledge on #javascript, #typescript, #angular, #ionic and #pwa
JS Squad Lead @LINKIT
Speaker, Organiser @ITNEXT
Speaker, Organiser @GDG NL
Chrome
Chrome
canarychromiumchrome
Headless Chrome
Headless Chrome
chrome —-headless —-remote-debugging-port=9222
Puppeteer
Puppeteer
npm i puppeteer
–Puppeteer docs
“Most things that you can do
manually in the browser can be
done using Puppeteer! ”
puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.linkit.nl');
  await page.screenshot({path: 'screenshot.png'});
  await browser.close();
});
pptr.dev
try-puppeteer.appspot.com
DEMOS
Side by side page load
const devices = require('puppeteer/DeviceDescriptors');
const nexus5X = devices['Nexus 5X'];
const browser = await puppeteer.launch({
headless: false,
args: [
`--window-size=${DEFAULT_VIEWPORT.width},${DEFAULT_VIEWPORT.height}`,
CENTER_WINDOWS_ON_SCREEN ? `--window-position=${x},${y}` : `--window-position=${dx},0`,
],
});
const page = await browser.newPage();
await page.emulate(nexus5X);
const session = await page.target().createCDPSession();
// Emulate "Slow 3G" according to WebPageTest
await session.send('Network.emulateNetworkConditions', {
offline: false,
latency: 400,
downloadThroughput: Math.floor(400 * 1024 / 8), // 400 Kbps
uploadThroughput: Math.floor(400 * 1024 / 8) // 400 Kbps
});
await session.send('Emulation.setCPUThrottlingRate', {rate: 4});
Side by side page load
Code coverage test
const pti = require('puppeteer-to-istanbul');
const page = await browser.newPage();
await page.coverage.startJSCoverage();
await page.goto('https://www.linkit.nl/');
const jsCoverage = await page.coverage.stopJSCoverage();
pti.write(jsCoverage);
await page.close();
Code coverage test
Chrome DevTools
throttle network
track memory usage
emulate devices
run audits
github.com/ChromeDevTools/devtools-frontend
Protocol Monitor
chrome://flags/#enable-devtools-experiments
chrome —-remote-debugging-port=9222
Chrome DevTools
Protocol
127.0.0.1:9222/json/version
chromedevtools.github.io/devtools-protocol
Using DevTools
Protocol with PPTR
puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.linkit.nl');
const session = await page.target().createCDPSession();
  session.on('Performance.metrics', (metrics, title) => {
    console.log({ metrics, title });
  });
  await session.send(‘Performance.enable');
  const metrics = await session.send('Performance.getMetrics');
  await browser.close();
});
DEMOS
even more
Budgets on metrics
Budgets on metrics
{ name: 'Timestamp', value: 66567.150449 },
{ name: 'AudioHandlers', value: 0 },
{ name: 'Documents', value: 8 },
{ name: 'Frames', value: 3 },
{ name: 'JSEventListeners', value: 34 },
{ name: 'LayoutObjects', value: 455 },
{ name: 'MediaKeySessions', value: 0 },
{ name: 'MediaKeys', value: 0 },
{ name: 'Nodes', value: 970 },
{ name: 'Resources', value: 74 },
{ name: 'ContextLifecycleStateObservers',
value: 0 },
{ name: 'V8PerContextDatas', value: 4 },
{ name: 'WorkerGlobalScopes', value: 0 },
{ name: 'UACSSResources', value: 0 },
{ name: 'RTCPeerConnections', value: 0 },
{ name: 'ResourceFetchers', value: 8 },
{ name: 'AdSubframes', value: 0 }
{ name: 'DetachedScriptStates', value: 2 },
{ name: 'LayoutCount', value: 13 },
{ name: 'RecalcStyleCount', value: 22 },
{ name: 'LayoutDuration', value: 0.067929 },
{ name: 'RecalcStyleDuration', value: 0.029508 },
{ name: 'ScriptDuration', value: 0.122922 },
{ name: 'V8CompileDuration', value: 0.003031 },
{ name: 'TaskDuration', value: 0.336774 },
{ name: 'TaskOtherDuration', value: 0.116415 },
{ name: 'ThreadTime', value: 0.275266 },
{ name: 'JSHeapUsedSize', value: 7816504 },
{ name: 'JSHeapTotalSize', value: 11096064 },
{ name: 'FirstMeaningfulPaint', value: 66565.452541 },
{ name: 'DomContentLoaded', value: 66565.386449 },
{ name: 'NavigationStart', value: 66564.624457 }
Performance.getMetrics
Budgets on metrics
[
{
"path": "/",
"perfMetrics": [
{
"metric": "JSEventListeners",
"budget": 100
},
{
"metric": "Nodes",
"budget": 2000
},
{
"metric": "JSHeapUsedSize",
"budget": 20000000
}
]
}
]
[
{
"path": "/vacatures",
"perfMetrics": [
{
"metric": "Resources",
"budget": 80
}
]
}
]
budget.json
const { getBudgetMetricsOfPage, getMatchedPageMetrics,
getBudgetMetricByPageMetricName } = require(‘./helpers');
const assertMetricsOnUrl = async (siteUrl) => {
const page = await this.browser.newPage();
const protocol = await page.target().createCDPSession();
await protocol.send('Performance.enable');
await page.goto(siteUrl, { waitUntil: 'networkidle0' });
const budgetMetrics = await getBudgetMetricsOfPage(page);
const { metrics: pageMetrics } = await protocol.send('Performance.getMetrics');
const matchedMetrics = getMatchedPageMetrics(pageMetrics, budgetMetrics);
matchedMetrics.forEach(pageMetric => {
expect(pageMetric.value).toBeLessThan(
getBudgetMetricByPageMetricName(budgetMetrics, pageMetric)
);
});
await page.close();
};
Budgets on metrics
beforeAll(async () => {
this.browser = await puppeteer.launch();
});
afterAll(async () => {
await this.browser.close();
});
test('asserts budget performance metrics on the main page', async() => {
await assertMetricsOnUrl('https://www.linkit.nl/');
});
test('asserts budget performance metrics on vacatures page', async() => {
await assertMetricsOnUrl('https://www.linkit.nl/vacatures');
});
Budgets on metrics
Monitoring
FPS Monitoring
const protocol = await page.target().createCDPSession();
await protocol.send('Overlay.setShowFPSCounter', { show: true });
await page.goto('https://www.linkit.nl');
// Do graphical regressions here by interacting with the page
await protocol.send('Input.synthesizeScrollGesture', {
x: 100,
y: 100,
yDistance: -400,
repeatCount: 3
});
await page.screenshot({
path: 'fps.jpeg',
type: 'jpeg',
clip: {
x:0,
y:0,
width: 370,
height: 370
}
});
FPS Monitoring
Memory leak by Heap
const protocol = await page.target().createCDPSession();
await protocol.send('HeapProfiler.enable');
await protocol.send('HeapProfiler.collectGarbage');
const startMetrics = await page.metrics();
// Do memory regressions here by interacting with the page
await protocol.send('Input.synthesizeScrollGesture', {
x: 100,
y: 100,
yDistance: -400,
repeatCount: 3
});
await protocol.send('HeapProfiler.collectGarbage');
const endMetrics = await page.metrics();
expect(endMetrics.JSHeapUsedSize < startMetrics.JSHeapUsedSize * 1.1)
.toBeTruthy();
Memory leak by Heap
Thank you!
@onderceylan
https://github.com/onderceylan/puppeteer-demos

Introducing perf budgets on CI with puppeteer - perf.now()