— What problem are we solving?
— A project with the best performance ever will loose its performance with each new feature.
— We need to prevent this kind of degradation!
— Best way to do it: measure performance on a regular basis.
— Choose metrics. Which one? It highly depends on your context.
— How to set target values? Make a list of your competitors and set your goals on their basis.
— When you are chased by a bear you don't need to be an Olympic champion in running.
— You just need to be faster than the other guy.
— Let's add to competitors metrics 20%. It's a magic number so user really may see the difference with the naked eye.
— There are two types of measurements: laboratory and real user measurements.
— We use laboratory measurements locally and in the CI
— Let's start with a bundle size as an easy pick
- This package is created by Andrey Sitnik and best pick for libraries.
npm i -D size-limit/preset-small-lib
// package.json
+ "size-limit": [
+ {
+ "path": "index.js"
+ }
+ ],
// package.json
"scripts": {
+ "size": "size-limit",
"test": "jest && eslint ."
}
npm run size
// package.json
"size-limit": [
{
+ "limit": "2 KB",
"path": "index.js"
}
],
# .gitlab-ci.yml
script:
- npm run size
artifacts:
expire_in: 7 days
paths:
- metric.txt
reports:
metrics: metric.txt
// package.json
"scripts": {
- "size": "size-limit",
+ "size": "size-limit --json
+ > size-limit.json",
+ "postsize": "node generate-metric.js
+ > metric.txt"
"test": "jest && eslint ."
},
// size-limit.json
[{
"name": "index.js",
"passed": true,
"size": 1957
}]
// generate-metric.js
const report = require('./size-limit.json');
process.stdout.write(`size ${(report[0].size/1024).toFixed(1)}`);
process.exit(0);
// metric.txt
size 1.19
metric1Name: currentValue1 (OldValue1)
metric2Name: currentValue2 (OldValue2)
"performance": {
"hints": "warning",
"maxEntrypointSize": 400000,
"maxAssetSize": 100000
}
webpack
--profile
--json
> stats.json
npm i -S @zeit/next-bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@zeit/next-bundle-analyzer');
module.exports = withBundleAnalyzer({/* … */});
// next.config.js
const nextConfig = {
analyzeBrowser: true,
bundleAnalyzerConfig: {/* … */},
webpack (config) {
return config;
}};
// next.config.js
browser: {
analyzerMode: 'disabled',
generateStatsFile: true,
statsFilename: 'client.json',
statsOptions: {/* … */},
}},
statsOptions: {
chunks: true,
assets: false,
cached: false,
cachedAssets: false,
chunkModules: false,
chunkOrigins: false,
entrypoints: false,
modules: false,
reasons: false }
npm install -D bundlesize
// bundlesize.config.json
{"files": [
{
"path": "./.next/static/chunks/*.js",
"maxSize": "300 kB"
},
…
]}
// package.json
"scripts": {
+ "test": "bundlesize",
}
npm t
# .travis.yml
language: node_js
node_js:
- 10
branches:
only:
- master
install:
- npm ci
- npm run build
script:
- npm t
travis encrypt BUNDLESIZE_GITHUB_TOKEN=token --add
# .gitlab-ci.yml
stages:
- performance
performance:
stage: performance
image: docker:git
services:
- docker:stable-dind
# .gitlab-ci.yml
script:
- mkdir gitlab-exporter
- wget -O ./gitlab-exporter/index.js
https://gitlab.com/gitlab-org
/gl-performance/raw/master/index.js
- mkdir sitespeed-results
- docker run --shm-size=1g --rm -v
"$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:9.8.1
--plugins.add ./gitlab-exporter
--outputFolder sitespeed-results
http://werkspot.nl/inloggen/
- mv sitespeed-results/data/performance.json
performance.json
# .gitlab-ci.yml
artifacts:
expire_in: 7 days
paths:
- performance.json
reports:
performance: performance.json
// performance.json
[{
"subject":"/",
"metrics":[{
"name":"Transfer Size (KB)",
"value":"19.5",
"desiredSize":"smaller"
}]}]
npm i -D lighthouse
npm i -D puppeteer
const browser =
await puppeteer.launch({
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--headless'
]});
const lighthouseConfig = {
port: (
new URL(browser.wsEndpoint())
).port,
output: 'json',
logLevel: 'info',
};
const auditConfig = {
extends: 'lighthouse:default',
settings: {
onlyCategories: ['performance']
}};
const auditConfig = {
extends: 'lighthouse:full'
};
const {
report: reportJSON,
lhr: {
audits
}} = await lighthouse(
'http://werkspot.nl/',
lighthouseConfig,
auditConfig);
if (audits["speed-index"].score < 0.75)
process.exit(1);
process.exit(0);
const {categories: {
performance: {
score
}
}} = JSON.parse(reportJSON);
if (score < 0.75)
process.exit(1);
process.exit(0);
const report = JSON.stringify([{
"subject": '/inloggen',
"metrics": metrics.map(metric => ({
"name": metric.title,
"value": metric.value,
"desiredSize": "larger",
}))
}]);
fs.writeFileSync(`./performance.json`, report);
# .gitlab-ci.yml
image: node:latest
stages:
- performance
lighthouse:
stage: performance
before_script:
- apt-get update
- apt-get -y install gconf-service …
- npm ci
script:
- node performance.js
artifacts:
expire_in: 7 days
paths:
- performance.json
reports:
performance: performance.json
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
await page.click('input[name=email]');
await page.keyboard.type(USER_EMAIL);
await page.click('input[name=password]');
await page.keyboard.type(USER_PASSWORD);
await page.click('button[type="submit"]',
{ waitUntil: 'domcontentloaded' });
Server-Timing:
cache;
desc="Cache Read";
dur=23.2
Server-Timing: cache
Chrome v60-64
Server-Timing:
cache=23.2;
"Cache Read"
const express = require('express');
const app = express();
app.get('/', (req, res) => {
//…
res.send(200).end();
});
app.listen(3000);
res.set(
'Server-Timing',
'cache;
desc="Cache Read";
dur=23.2');
// node.js v0.7.6
process.hrtime();
// integer[]
// [ seconds, nanoseconds ]
// [ 8063, 904046009 ]
const from = process.hrtime();
const duration = process.hrtime(from);
// To milliseconds
const ms = parseInt(
duration[0] * 1e3 +
duration[1] * 1e-6,
10);
// node.js v10.7.0, Stage 3
const from = process.hrtime.bigint();
const to = process.hrtime.bigint();
const duration = to - from;
// To milliseconds
const ms = duration / 1000000n;
npm i -S server-timing-header
const express = require('express');
const app = express();
app.get('/', (req, res) => {
//…
res.send(200).end();
});
app.listen(3000);
+ const serverTiming =
+ require('server-timing-header');
const express = require('express');
const app = express();
+ app.use(serverTiming());
app.get('/', (req, res) => {
//…
req.serverTiming.from('db');
// fetching data from database
req.serverTiming.to('db');
req.serverTiming.add(
'cache', // name
'Cache Read', //description
23.2 // value
);
req.serverTiming.add(
'cache',
'Element not in cache',
);
// not safari
// ff need https
performance.getEntriesByType('navigation');
performance.getEntriesByType('resource');
Timing-Allow-Origin: *
import { ApolloLink } from 'apollo-link';
import uuidv4 from 'uuid/v4';
const serverTimingLink = request =>
new ApolloLink((operation, forward) => {
/* … */
});
export default serverTimingLink;
const id = uuidv4();
if (request) request.serverTiming.from(
`${operation.operationName}-${id}`,
operation.operationName
);
return forward(operation).map(data => {
if (request) request.serverTiming.to(
`${operation.operationName}-${id}`
);
return data;
});
Performance
PerformanceObserver
PerformanceObserver.supportedEntryTypes
performance.getEntriesByType(entryType)
performance.getEntriesByName(name)
performance.getEntries()
const observer = new PerformanceObserver(handler);
observer.observe({ entryTypes });
performance.now()
performance.timeOrigin
const start = performance.now();
const end = performance.now();
const duration = end - start;
performance.mark("startTask1");
// Fetching data
const url = '/api/v1/employees';
const resource = await fetch(url);
performance.mark("endTask1");
const entries = performance
.getEntriesByType("mark");
for (const entry of entries) {
console.table(entry.toJSON());
}
performance.measure(
'task1',
'startTask1',
'endTask1'
);
const entries = performance
.getEntriesByType("measure");
for (const entry of entries) {
console.table(entry.toJSON());
}
performance.clearMeasures('task1')
performance.measure('from start');
const entries = performance
.getEntriesByType("measure");
for (const entry of entries) {
console.table(entry.toJSON());
}
performance.navigation
const entries = performance
.getEntriesByType("resource");
for (const entry of entries) {
console.table(entry.toJSON());
}
const entries = performance
.getEntriesByType("paint");
for (const entry of entries) {
console.table(entry.toJSON());
}
const observer = new PerformanceObserver(
(list, observer)=>{
const perfEntries = list.getEntries();
for (var i = 0; i < perfEntries.length; i++) {
console.log(perfEntries[i]);
}});
observer.observe({ entryTypes: ['longtask'] });
const express = require('express');
const multer = require('multer');
const port = 3333;
const app = express();
app.use('/reports',
multer().none(),
function (req, res) {
res.status(204).end();
console.log(req.body);
});
app.use('/', function (req, res) {
res.send(`
<script>
/* … */
</script>
`).status(200).end();
});
app.listen(port);
let data = new FormData();
let items = {
some: 'data',
more: ['words', 'aside'],
};
for ( var key in items ) {
data.append(
key,
items[key]
);
}
navigator.sendBeacon('/reports', data);
{
some: 'data',
more: 'words,aside'
}
Anton Nemtsev
http://silentimp.info/
@silentimp
thesilentimp@gmail.com
skype: ravencry