Filippo Matteo Riggio

CTO @ Kaleidoscope Srl

Teacher @ ITS Logistica

Performance Obsessed

Performance Obsessed

  • 2011: Gardaland

Performance Obsessed

  • 2011: Gardaland

  • 2012: ***

Performance Obsessed

  • 2011: Gardaland

  • 2012: ***

  • 2013: BMW

  • 2014: Microsoft World Cup

Performance Obsessed

  • 2011: Gardaland

  • 2012: ***

  • 2013: BMW

  • 2014: Microsoft World Cup

  • 2014: The Sun

  • 2014: Future Learn

Performance Obsessed

  • 2011: Gardaland

  • 2012: ***

  • 2013: BMW

  • 2014: Microsoft World Cup

  • 2014: The Sun

  • 2014: Future Learn

  • 2015: Oltreuomo

  • 2016: Le Iene

Frontend

75%

Backend

25%

L'importanza delle performance

Il 47% degli utenti si aspetta che un sito si carichi in meno di 2 secondi

 

Il 40% degli utenti abbandona un sito se il tempo di caricamento è più di 3 secondi

 

+2% di conversione sullo store di Walmart per ogni secondo guadagnato

 

1.6 Miliardi di dollari persi per ogni secondo di rallentamento su Amazon.com 

Conversioni

Impact Calculator

Gli ute(o)nti

L'esperienza degli utenti

< 100 millisecondi

  • Il lobo occipitale si attiva entro questo lasso di tempo
  • L'utente ha la percezione dell'azione e della reazione e l'illusione che la web app sia istantanea
  • L'utente ha senso di soddisfazione

> 10 secondi

  • Il lobo occipitale perde attrattiva su quello che sta facendo
  • L'utente ha la sensazione di  rottura tra azione e reazione della web app
  • L'utente diventa frustrato e impaziente
  • La percezione è che la web app sia di pessima qualità

Per ottimizzare sempre si consideri il modello RAIL (Response, Animation, Idle, Load)  

Misurazione

DevTools

PageSpeed

Google Lighthouse

An open-source, automated tool for improving the quality of web pages

Cosa misura?

Accessibilità

SEO

  • Attributi ARIA
  • Tag title nel tag header
  • Tag label collegate ad ogni elemento di un form
  • Ogni immagine ha l'attributo alt
  • Tag title nel tag header
  • Meta description mancante
  • hreflang mancante
  • rel=canonical non valido
  • robots.txt non valido
  • ecc.

Cosa misura?

PWA

Performance

  • Verifica il file manifest.json
  • La connessione è sicura (HTTPS)
  • Viene registrato un service worker
  • Altri controlli propri delle PWA

 

https://developers.google.com/web/progressive-web-apps/checklist

  • Critical Request Chains
  • CSS minificato
  • JS minificato
  • Evitare le richieste Render-blocking
  • ecc.

Installazione

- Chrome DevTools > Audits

- comando da CLI

- modulo NPM

- Lighthouse CI

 

# SUL TERMINALE
npm install -g lighthouse

# LANCIARE UN AUDIT

lighthouse <url>

Installiamo prima :

- Google Chrome

- Node.JS LTS

Installazione

Un esempio di audits

Installazione e setup

# SUL TERMINALE
yarn add webpack webpack-cli --dev

// package.json
"scripts": {
    "dev": "webpack --mode development --watch",
    "build": "webpack --mode production --watch"
}

# SUL TERMINALE
yarn dev

Configurazione

// webpack.config.js

module.exports = {
    entry: './src/index.js'
};

Entry

Suggerisce a webpack da quale modulo iniziare il bundling.

Output

Suggerisce a webpack dove salvare il bundle creato.

// webpack.config.js

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'main.js'
    }
};

Configurazione

Entry e output multipli

Suggerisce a webpack da quale modulo iniziare il bundling.

// webpack.config.js

const path = require('path'); 
 
module.exports = {
    entry: {
        first: './src/one.js',
        second: './src/two.js',
    },
    output: {
        filename: '[name].bundle.js', // GENERA first.bundle.js, second.bundle.js, ecc.
        path: path.resolve(__dirname, 'dist')
    }
}

Loaders

Oltre che a creare bundle, con webpack è possibile effettuare altre operazioni in fase di bundling attraverso i loaders.

# TERMINALE
yarn add css-loader --dev

// webpack.config.js

module.exports = {
    [...],
    module: {
        rules: [{
            test: /\.css$/,
            use: 'css-loader'
        }]
    }
}

rules è un array di tutti le regole e dei loader da utilizzare a tutti i file che matchano la proprietà test.

use indica quale loader usare nel caso il file matchasse la regola

Concatenare Loader

# TERMINALE
yarn add node-sass sass-loader style-loader --dev

// webpack.config.js

module.exports = {
    [...],
    module: {
        rules: [{
            test: /\.(sass|scss)$/,
            use: [{
                loader: 'style-loader' // creates style nodes from JS strings
            }, {
                loader: 'css-loader' // translates CSS into CommonJS
            }, {
                loader: 'sass-loader', // compiles Sass to CSS
                options: {
                    includePaths: [
                        __dirname + '/resources/assets/sass/frontend/'
                    ]
                }
            }]
        }]
    }
}

Un esempio

// style.scss
body {
    background-color: $red;
}
// index.js
import './style.scss';
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
    <style type="text/css">
        body {
            background-color:red;
        }
    </style>
</head>
<body>
    <script type="text/javascript" src="bundle.js"></script>
</body>
</html>
  1. Webpack prova a risolvere style.scss
  2. Il nome del file matcha /\.(sass|scss)$/
  3. Il file viene interpretato dal loader "sass-loader", il risultato poi passato a css-loader
  4. Il risultato dal "css-loader" viene passato allo "style-loader"
  5. "style-loader" restituisce codice js e webpack continua la sua esecuzione

Babel Loader / TypeScript Loader

# TERMINALE
yarn add babel-core babel-loader babel-preset-env ts-loader --dev

// webpack.config.js

module.exports = {
    [...],
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /(node_modules)/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                }
            }
        },{
            test: /\.tsx?$/,
            use: 'ts-loader',
            exclude: [
                '/node_modules/',
                '/vendor/'
            ]
        }]
    }
}

Plugins

# TERMINALE
yarn add webpack-manifest-plugin mini-css-extract-plugin --dev

// webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ManifestPlugin       = require('webpack-manifest-plugin');

module.exports = {
    [...],
    module: {
        rules: [{
            test: /\.(sass|scss)$/,
            use: [
                "style-loader",
                MiniCssExtractPlugin.loader,
                "css-loader",
                "sass-loader"
            ]
        }]
    },
    plugins: [
        new ManifestPlugin(),
        new MiniCssExtractPlugin({
            filename: 'style.css',
        })
    ]
}

Caching e Hashing

// webpack.config.js

module.exports = {
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[chunkhash].js'
    },
}

Optimization - Minification

// webpack.config.js
const config = {
    [...]
};

module.exports = (env, argv) => {

    config.mode = argv.mode;

    config.optimization = {
        minimize: true,
        minimizer: [
            new TerserPlugin()
        ]
    };
    
    return config;
};

Minimizer disponibili:

  • UglifyJS
  • Babel-minify
  • Terser

Optimization - NoEmitsOnErrorsPlugin

// webpack.config.js
module.exports = {
    [...],
    plugins: [
        new webpack.NoEmitOnErrorsPlugin();
    ];
};

Se webpack, durante la compilazione, genera un errore, questo viene incluso del bundle.

Con il plugin NoEmitOnErrors, questo viene evitato.

Optimization - ModuleConcatenationPlugin

// webpack.config.js
module.exports = {
    [...],
    plugins: [
        new webpack.optimization.ModuleConcatenationPlugin()
    ];
};

Webpack, in modalità dev, wrappa ogni modulo in una closure, rendendo l'esecuzione più lenta.

Con questo plugin (ormai abilitato di default) è possibile evitare le generazione di ogni singola closure, diminuendo i tempi di esecuzione.

Split Chunks

//./utilities/users.js
export default [
    { firstName: "Adam", age: 28 },
    { firstName: "Jane", age: 24 },
    { firstName: "Ben",  age: 31 },
    { firstName: "Lucy", age: 40 }
];
// first.js
import _ from 'lodash';
import users from './users';
 
const adam = _.find(users, { firstName: 'Adam' });
// second.js
import _ from 'lodash';
import users from './users';
 
const lucy = _.find(users, { firstName: 'Lucy' });
// webpack.config.js
module.exports = {
    entry: {
        first:  "./src/first.js",
        second: "./src/second.js"
    },
    output: {
        filename: "[name].[chunkhash].bundle.js",
        path: __dirname + "/dist"
    }
};

WebPack genererà : first.[chunkhash].bundle.js e second.[chunkhash].bundle.js; entrambi contengono lodash e users.js

Split Chunks

// webpack.config.js
module.exports = {
    [...],
    optimization: {
        splitChunks: {
            chunks: "all"
        }
    },
};

WebPack genererà :

  • vendors~first~second.[chunkhash].bundle.js
  • first.[chunkhash].bundle.js
  • second.[chunkhash].bundle.js

Entrambi first.[chunkhash].bundle.js e second.[chunkhash].bundle.js contengono users.js

Split Chunks

// webpack.config.js
module.exports = {
    [...]

    splitChunks: {
        chunks: "all",
        minSize: 30,
        cacheGroups: {
            vendors: {
                test: /[\\/]node_modules[\\/]/,
                priority: -10
            },
            default: {
                minChunks: 2,
                priority: -20,
                reuseExistingChunk: true
            }
        }
    }

}

Split Chunks

// webpack.config.js
module.exports = {
    [...],
    optimization: {
        splitChunks: {
            chunks: "all",
            minSize: 0 // Webpack genererà first~second.[chunkhash].bundle.js
        }
    },
};

WebPack genererà :

  • vendors~first~second.[chunkhash].bundle.js
  • first~second.[chunkhash].bundle.js
  • first.[chunkhash].bundle.js
  • second.[chunkhash].bundle.js

Google Lighthouse Webpack Plugin

Installazione e setup

# SUL TERMINALE
yarn add --save-dev webpack-lighthouse-plugin
// webpack.config.js

const WebpackLighthousePlugin = require('webpack-lighthouse-plugin');

module.exports = {
    [...]


	plugins: [
		new WebpackLighthousePlugin({
			url: 'http://localhost:9001'
		})
	]

}

API

// webpack.config.js

const WebpackLighthousePlugin = require('webpack-lighthouse-plugin');

module.exports = {
    [...]


	plugins: [
		new WebpackLighthousePlugin({
			url: 'http://localhost:9001',
			perf: true, // EFFETTUA L'AUDIT SOLO DELLE PERFORMANCE
			disableCPUThrottling: false, // DISABILITA LO STRESS TEST SULLA CPU
			disableNetworkThrottling: true, // DISABILITA LO STRESS TEST SULLA CONNESSIONE
			saveAssets: true, // SALVA SCREENSHOT, TRACE O REPORT 
		})
	]

}

Un caso reale

www.kaleidoscope.it

Strumenti Utilizzati

Librerie usate

Performance pre-ottimizzazioni

Mobile

Bundle JS: 4.7 MB

Immagini: 445KB

Altro (html, css, fonts, ecc.): 30KB

Totale: 5.16 MB

Desktop

Bundle JS: 4.7 MB

Immagini: 3.7MB

Altro (html, css, fonts, ecc.): 30KB

Totale: 8.42 MB

Coverage

Webpack Bundle Analyzer

Librerie usate

Ottimizzazioni

Above the fold

Problema: Render-blocking

<head>
    <link rel="stylesheet" href="/style.css">
    ...
</head>
<body>
    <p>Non visibile fino a che non ho caricato style.css!</p>
</body>

Soluzione: Critical CSS

# TERMINALE
yarn add html-critical-webpack-plugin -d

// webpack.config.js
module.exports = {
    [...],
    plugins: [
        new HtmlWebpackPlugin({ ... }),
        new MiniCssExtractPlugin({ ... }),
        new HtmlCriticalWebpackPlugin({
            base: path.resolve(__dirname, 'dist'),
            src: 'index.html',
            dest: 'index.html',
            inline: true,
            minify: true,
            extract: true,
            width: 375,
            height: 565,
            penthouse: {
                blockJSRequests: false,
            }
        })
    ],
    [...]
};
  1. Viene letto il file definito in src
  2. Estrae il CSS definito "critico"
  3. Scrive il nuovo file con il CSS in linea, sul file definito in dest

Soluzione: Critical CSS

<!-- index.html -->
<head>
    <style type="text/css">
        /* Il CSS definito critico viene messo in linea nell'head. */
        body {
            font-family: Helvetica Neue,Helvetica, Arial,sans-serif;
            font-size: 14px;
            line-height: 1.42857;
            color: #333;
            background-color: #fff;
        }    
    </style>
    <link href="/style.96106fab.css" rel="preload" as="style" onload="this.rel='stylesheet'">
    <noscript>
        <link href="/style.96106fab.css" rel="stylesheet">
    </noscript>
    <script>
        /* Uno script si occupa di caricare il CSS "non critico" */
    </script>
</head>
<body>
	[...]
    <script type="text/javascript" src="/build_main.js"></script>
</body>
</html>

Resource hinting

Tutta questione di priorità

Preload

<link rel="preload" as="script" href="super-important.js">
<link rel="preload" as="style" href="critical.css">
<link rel="preload" as="font" crossorigin="crossorigin" type="font/woff2" href="myfont.woff2">

rel="preload" : recupera questa risorsa il prima possibile

as="script" : suggerisco al browser già che tipo di risorsa è questa

crossorigin="crossorigin" : effettua questa richiesta come una CORS anonima, altrimenti il font viene caricato 2 volte

Preconnect

<link rel="preconnect" href="https://example.com">

rel="preconnect" : suggeriamo al browser che probabilmente servirà una risorsa reperibile all'URL specificato, il browser eviterà roundtrip inutili

 

1 roundtrip = lookup del DNS + negoziazione TLS + handshake TCP

 

rel="preconnect" torna utile nell'uso delle CDN

DNS-prefetch

<link rel="dns-prefetch" href="https://example.com">

rel="dns-prefetch" : suggeriamo al browser di fare già il DNS Lookup dell'URL

 

A differenza del preconnect, dns-prefetch effettua SOLO il lookup del DNS.

Prefetch

<link rel="prefetch" href="page-2.html">

rel="prefetch" : suggeriamo al browser che probabilmente useremo la risorsa oppure no (a seconda di dinamiche dovute alle scelte dell'utente).

E' un modo per aiutare il browser a gestire le risorse in modo intelligente.

NON va usato, per specificare delle risorse come non prioritarie.

<head>
    <link rel="prefetch" href="optional.css"><!-- CARICATO CON PRIORITA BASSA, PROBABILE CACHE HIT -->
    <link rel="stylesheet" href="optional.css"><!-- CARICATO CON PRIORITA MASSIMA -->
</head>

Performance post-ottimizzazioni

Mobile

Bundle JS: 831 KB

Immagini: 450KB

Altro (html, css, fonts, ecc.): 30KB

Totale: 1.28 MB

Desktop

Bundle JS: 831 KB

Immagini: 3.7MB

Altro (html, css, fonts, ecc.): 30KB

Totale: 4.54 MB

Immagini

  • Usare i formati corretti: svg, gif

  • Usare i nuovi formati: WebP / JPEG2000

  • Usare le dimensioni corrette

  • Usare CDN: Akamai Image Manager / imgix / cloudinary

  • Comprimere, comprimere, comprimere

  • Sostituire icone con SVG

  • Lazy load delle immagini

Inserire in linea PNG, GIF e JPG di piccole dimensioni

// webpack.config.js

module.exports = {
    module: {
        rules: [{
            test: /\.(jpe?g|png|gif)$/,
            loader: 'url-loader',
            options: {
                // Inserisce in linea immagini fino a 10 KB di peso
                limit: 10 * 1024
            }
        }]
    }
};

Il loader converte le immagini in Base 64

Inserire in linea SVG di piccole dimensioni

// webpack.config.js

module.exports = {
    module: {
        rules: [{
            test: /\.svg$/,
            loader: 'svg-url-loader',
            options: {
                // Inserisce in linea immagini fino a 10 KB di peso
                limit: 10 * 1024,
                // Rimuove gli apici dall'URL codificato in base64
                noquotes: true,
            }
        }]
    }
};

Compressione delle immagini

// webpack.config.js

module.exports = {
    module: {
        rules: [{
            test: /\.(jpg|png|gif|svg)$/,
            loader: 'image-webpack-loader',
            enforce: 'pre'
        }]
    }
};

enforce applica il loader prima di url-loader/svg-url-loader

Pros & Cons

PRO

  • Webpack rende facile la gestione del bundling, il controllo degli assets e l'ottimizzazione di tutti i componenti di un progetto frontend

  • Ha già tante opzioni/funzionalità al suo interno

  • Webpack-dev-server con livereload e HMR

CONS

  • Curva di apprendimento

  • Difficoltà nel trovare la configurazione giusta

Approfondimenti

Strumenti

Credits

Marco Guarino

Full Stack Developer @KaleidoscopeSrl

Valentina Piacenza

Junior UX Designer @Fondazione Edulife

Grazie!

Google LightHouse e Webpack. Performance obsession.

By Filippo Matteo Riggio

Google LightHouse e Webpack. Performance obsession.

Il talk è incentrato su come ottimizzare un sito / web app in termini di performance usando al meglio WebPack e le ultime novità introdotte dal W3C. Per chi non conoscesse lo strumento, è previsto un mini crash course per imparare i concetti base dello strumento.

  • 949