Performance

Problem

— What problem are we solving?

DegradationDegradation

— A project with the best performance ever will loose its performance with each new feature.

Goal

Prevent

— We need to prevent this kind of degradation!

Measure

— Best way to do it: measure performance on a regular basis.

Metrics

  • Time to first byte
  • Total byte weight
  • Network request count
  • First meaningful paint
  • Speed index
  • First input delay
  • Time to interactive
  • Bundle size

— Choose metrics. Which one? It highly depends on your context.

Competitors will help

  • Marktplaats
  • Kluswebsite
  • Casius
  • Zoofy
  • Skydreams
  • Solvari
  • Yonego
  • Offerte.nl

— 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.

+20%

— Let's add to competitors metrics 20%. It's a magic number so user really may see the difference with the naked eye.

Measurements types

lab
lab

— There are two types of measurements: laboratory and real user measurements.

continuous
integration

— We use laboratory measurements locally and in the CI

Bundle size

— 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 Artifacts
QRCODE with link

https://bit.ly/2knRWgE

# .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
Prometheus text-based format
QRCODE with link

https://bit.ly/2kbi6mZ

metric1Name: currentValue1 (OldValue1)
metric2Name: currentValue2 (OldValue2)
Example with library
QRCODE with link

https://bit.ly/2jVaDZ3

"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 }
Example with next.js
QRCODE with link

https://bit.ly/2jVaDZ3

logotype
logotype
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
Give bundlesize app access to your repos
QRCODE with link

https://bit.ly/2kpdVnA

travis encrypt BUNDLESIZE_GITHUB_TOKEN=token --add

site
speed.io

How to use sitespeed in GitLab CI
QRCODE cо ссылкой

https://bit.ly/2kbYhfj

# .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"
    }]}]
Sitespeed example
QRCODE cо ссылкой

https://bit.ly/2kbWqHn

lighthouse

logotype
logotype
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
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
artifacts:
    expire_in: 7 days
    paths:
        - performance.json
    reports:
        performance: performance.json

Lighthouse example (the cool one)
QRCODE cо ссылкой

https://bit.ly/2m6m80d

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

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;
});

Perfor
mance

Performance
PerformanceObserver
  • mark
  • measure
  • navigation
  • firstInput
  • frame
  • resource
  • longtask
  • layoutShift
  • paint
PerformanceObserver.supportedEntryTypes
performance.getEntriesByType(entryType)
performance.getEntriesByName(name)
performance.getEntries()
const observer = new PerformanceObserver(handler);
observer.observe({ entryTypes });

High Res.
Time

performance.now()
performance.timeOrigin
const start = performance.now();
const end = performance.now();
const duration = end - start;

User
Timing

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

Resource
Timing

const entries = performance
    .getEntriesByType("resource");
for (const entry of entries) {
    console.table(entry.toJSON());
}

Paint

const entries = performance
    .getEntriesByType("paint");
for (const entry of entries) {
    console.table(entry.toJSON());
}

Long
Task

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'] });

Layout
Instability
API

Layout Instability API
QRCODE cо ссылкой

https://bit.ly/2lyJOKy

Measuring Real User Performance in the Browser
QRCODE cо ссылкой

https://www.youtube.com/watch?v=yrWLi524YLM

Frame
Timing

Battery
Status
API

Network
Information
API

Network Information API
QRCODE cо ссылкой

https://wicg.github.io/netinfo/

Beacon

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'
}
World Wide Web Consortium
QRCODE cо ссылкой

https://github.com/w3c/

Web Incubator CG
QRCODE cо ссылкой

https://github.com/WICG/

https://careersatwerkspot.com/vacancies/

Anton Nemtsev

skype: ravencry

QRCODE cо ссылкой на доклад

https://bit.ly/2lQrJYt