Come utilizzare webpack per ridurre al minimo le dimensioni dell'app
Una delle prime cose da fare quando si ottimizza un'applicazione è ridurla il più possibile. Ecco come fare con webpack.
Utilizzare la modalità di produzione (solo Webpack 4)
Webpack 4 ha introdotto il nuovo flag mode
. Puoi impostare
il flag su 'development'
o 'production'
per suggerire a webpack che stai creando
l'applicazione per un ambiente specifico:
// webpack.config.js
module.exports = {
mode: 'production',
};
Assicurati di attivare la modalità production
quando crei la tua app per la produzione.
In questo modo, webpack applicherà ottimizzazioni come la minimizzazione, la rimozione del codice solo per lo sviluppo nelle librerie e altro ancora.
Per approfondire
Abilita minimizzazione
La minimizzazione consiste nel comprimere il codice rimuovendo gli spazi extra, accorciando i nomi delle variabili e così via. Esempio:
// Original code
function map(array, iteratee) {
let index = -1;
const length = array == null ? 0 : array.length;
const result = new Array(length);
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
↓
// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}
Webpack supporta due modi per minimizzare il codice: minificazione a livello di bundle e opzioni specifiche del caricatore. Devono essere utilizzati contemporaneamente.
Minimizzazione a livello di bundle
La minimizzazione a livello di bundle comprime l'intero bundle dopo la compilazione. Ecco come funziona:
Scrivi il codice in questo modo:
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
Webpack lo compila approssimativamente in base ai seguenti elementi:
// bundle.js (part of) "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["render"] = render; /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__); function render(data, target) { console.log('Rendered!'); }
Un compressore lo comprime in modo approssimativo nel seguente modo:
// minified bundle.js (part of) "use strict";function t(e,n){console.log("Rendered!")} Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
In webpack 4, la minimizzazione a livello di bundle viene attivata automaticamente sia in modalità di produzione sia senza. Utilizza in background il minificatore UglifyJS. Se devi disattivare la minimizzazione, utilizza la modalità di sviluppo o passa false
all'opzione optimization.minimize
.
In webpack 3, devi utilizzare direttamente il plug-in UglifyJS. Il plug-in è incluso in webpack; per attivarlo, aggiungilo alla plugins
sezione della configurazione:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
Opzioni specifiche del caricatore
Il secondo modo per minimizzare il codice è rappresentato da opzioni specifiche del caricatore (cos'è un caricatore). Con le opzioni del caricatore puoi comprimere
gli elementi che il minificatore non è in grado di minimizzare. Ad esempio, quando importi un file CSS con css-loader
, il file viene compilato in una stringa:
/* comments.css */
.comment {
color: black;
}
// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n color: black;\r\n}",""]);
Lo strumento di minificazione non può comprimere questo codice perché è una stringa. Per ridurre al minimo i contenuti del file, dobbiamo configurare il caricatore in modo da:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
Per approfondire
- Documentazione di UglifyJsPlugin
- Altri compressori popolari: Babel Minify, Google Closure Compiler
Specifica NODE_ENV=production
Un altro modo per ridurre le dimensioni del front-end è impostare la variabile di ambiente NODE_ENV
nel codice sul valore production
.
Le librerie leggono la variabile NODE_ENV
per rilevare in quale modalità devono funzionare: in fase di sviluppo o di produzione. Alcune librerie si comportano in modo diverso in base a questa variabile. Ad esempio, se NODE_ENV
non è impostato su production
, Vue.js esegue controlli aggiuntivi e visualizza gli avvisi:
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …
La reazione funziona in modo simile: carica una build di sviluppo che include gli avvisi:
// react/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
// react/cjs/react.development.js
// …
warning$3(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
// …
Questi controlli e avvisi in genere non sono necessari in produzione, ma rimangono nel codice e
aumentano le dimensioni della libreria. In webpack 4, rimuovili aggiungendo
l'opzione optimization.nodeEnv: 'production'
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
nodeEnv: 'production',
minimize: true,
},
};
In webpack 3, utilizza invece DefinePlugin
:
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.optimize.UglifyJsPlugin()
]
};
Entrambe l'opzione optimization.nodeEnv
e DefinePlugin
funzionano allo stesso modo:
sostituiscono tutte le occorrenze di process.env.NODE_ENV
con il valore specificato. Con la configurazione riportata sopra:
Webpack sostituirà tutte le occorrenze di
process.env.NODE_ENV
con"production"
:// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if (process.env.NODE_ENV !== 'production') { warn('props must be strings when using array syntax.'); }
↓
// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
Poi il minificatore rimuoverà tutti i rami
if
, perché"production" !== 'production'
è sempre falso e il plug-in comprende che il codice all'interno di questi rami non verrà mai eseguito:// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
↓
// vue/dist/vue.runtime.esm.js (without minification) if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; }
Per approfondire
- Cosa sono le "variabili di ambiente"
- Documenti Webpack relativi a:
DefinePlugin
,EnvironmentPlugin
Utilizzare i moduli ES
Un altro modo per ridurre le dimensioni del front-end è utilizzare i moduli ES.
Quando utilizzi i moduli ES, webpack diventa in grado di fare "albero di albero". Si verifica quando un bundler attraversa l'intero albero delle dipendenze, controlla quali dipendenze vengono utilizzate e rimuove quelle inutilizzate. Pertanto, se utilizzi la sintassi del modulo ES, webpack può eliminare il codice inutilizzato:
Scrivi un file con più esportazioni, ma l'app ne utilizza solo una:
// comments.js export const render = () => { return 'Rendered!'; }; export const commentRestEndpoint = '/rest/comments'; // index.js import { render } from './comments.js'; render();
Webpack riconosce che
commentRestEndpoint
non viene utilizzato e non genera un punto di esportazione separato nel bundle:// bundle.js (part that corresponds to comments.js) (function(module, __webpack_exports__, __webpack_require__) { "use strict"; const render = () => { return 'Rendered!'; }; /* harmony export (immutable) */ __webpack_exports__["a"] = render; const commentRestEndpoint = '/rest/comments'; /* unused harmony export commentRestEndpoint */ })
Il minificatore rimuove la variabile inutilizzata:
// bundle.js (part that corresponds to comments.js) (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
Questo funziona anche con le librerie se sono scritte con moduli ES.
Tuttavia, non è necessario utilizzare esattamente il compressore integrato di webpack (UglifyJsPlugin
).
È sufficiente qualsiasi minificatore che supporti la rimozione di codice obsoleto (ad es. il plug-in Babel Minify o il plug-in Google Closure Compiler).
Per approfondire
Documentazione di Webpack sul tree shaking
Ottimizza immagini
Le immagini rappresentano più della
metà delle dimensioni della pagina. Anche se non sono critici come JavaScript (ad es. non bloccano il rendering), occupano comunque gran parte della larghezza di banda. Utilizza url-loader
, svg-url-loader
e image-webpack-loader
per ottimizzarle in
webpack.
url-loader
inserisce nell'app piccoli file statici. Senza configurazione, prende un file superato, lo inserisce accanto al bundle compilato e restituisce l'URL del file. Tuttavia, se specifichiamo l'opzione limit
, i file di dimensioni inferiori a questo limite verranno codificati come URL di dati Base64 e restituirà questo URL. In questo modo l'immagine viene incorporata nel codice JavaScript e viene salvata una richiesta HTTP:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
},
},
],
}
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`
svg-url-loader
funziona come url-loader
,
ma solo per il fatto che codifica i file con la codifica
degli URL anziché con Base64. Questo è utile per le immagini SVG, poiché i file SVG sono semplicemente testo normale, questa codifica è più efficace per le dimensioni.
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: "svg-url-loader",
options: {
limit: 10 * 1024,
noquotes: true
}
}
]
}
};
image-webpack-loader
comprime le immagini
che lo attraversano. Supporta le immagini JPG, PNG, GIF e SVG, quindi la utilizzeremo per tutti questi tipi.
Questo caricatore non incorpora immagini nell'app, quindi deve funzionare in combinazione con url-loader
e
svg-url-loader
. Per evitare di copiarlo e incollarlo in entrambe le regole (una per le immagini JPG/PNG/GIF e un'altra per quelle SVG), includeremo questo caricatore come regola separata con enforce: 'pre'
:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// This will apply the loader before the other ones
enforce: 'pre'
}
]
}
};
Le impostazioni predefinite del caricatore sono già pronte per l'uso, ma se vuoi configurarlo ulteriormente, consulta le opzioni del plug-in. Per scegliere le opzioni da specificare, consulta l'eccellente guida all'ottimizzazione delle immagini di Addy Osmani.
Per approfondire
Ottimizza le dipendenze
Più della metà della dimensione media di JavaScript deriva dalle dipendenze e una parte di questa dimensione potrebbe essere semplicemente inutile.
Ad esempio, Lodash (a partire dalla versione 4.17.4) aggiunge 72 KB di codice compresso al bundle. Tuttavia, se ne utilizzi solo 20, circa 65 KB di codice compresso non fanno nulla.
Un altro esempio è Moment.js. La sua versione 2.19.1 richiede 223 kB di codice minimizzato, il che è enorme: la dimensione media di JavaScript in una pagina era di 452 kB nell'ottobre 2017. Tuttavia, 170 kB di quella dimensione sono file di localizzazione. Se non utilizzi Moment.js con più lingue, questi file gonfieranno il bundle senza scopo.
Tutte queste dipendenze possono essere facilmente ottimizzate. Abbiamo raccolto gli approcci all'ottimizzazione in un repository GitHub. Dai un'occhiata!
Attiva la concatenazione dei moduli per i moduli ES (noto anche come sollevamento degli ambiti)
Quando crei un bundle, webpack inserisce ogni modulo in una funzione:
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓
// bundle.js (part of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_exports__["a"] = render;
function render(data, target) {
console.log('Rendered!');
}
})
In passato, questa operazione era necessaria per isolare i moduli CommonJS/AMD l'uno dall'altro. Tuttavia, questo ha aggiunto un overhead di dimensioni e prestazioni per ogni modulo.
Webpack 2 ha introdotto il supporto per i moduli ES che, a differenza dei moduli CommonJS e AMD, possono essere raggruppati senza wrapping con una funzione. Inoltre, webpack 3 ha reso possibile questo raggruppamento, grazie alla concatenamento dei moduli. Ecco cosa fa la concatenazione dei moduli:
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓
// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files
// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// CONCATENATED MODULE: ./comments.js
function render(data, target) {
console.log('Rendered!');
}
// CONCATENATED MODULE: ./index.js
render();
})
Capisci la differenza? Nel bundle semplice, il modulo 0 richiedeva render
del modulo 1. Con la concatenazione del modulo, require
viene semplicemente sostituito con la funzione richiesta e il modulo 1 viene rimosso. Il bundle ha meno moduli e un minore overhead dei moduli.
Per attivare questo comportamento, in webpack 4, abilita l'opzione optimization.concatenateModules
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true
}
};
In webpack 3, utilizza ModuleConcatenationPlugin
:
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
Per approfondire
- Documentazione di Webpack per il plug-in ModuleConcatenationPlugin
- "Breve introduzione al sollevamento dell'ambito"
- Descrizione dettagliata di cosa fa questo plug-in
Utilizza externals
se hai codice sia webpack che non webpack
Potresti avere un progetto di grandi dimensioni in cui parte del codice viene compilata con webpack e parte no. Ad esempio, un sito di hosting video, in cui il widget del player potrebbe essere creato con webpack e la pagina circostante potrebbe non esserlo:
Se entrambe le parti del codice hanno dipendenze comuni, puoi condividerle per evitare di scaricare il codice
più volte. Per farlo, usa l'opzione externals
del webpack, che sostituisce i moduli con variabili o
altre importazioni esterne.
Se le dipendenze sono disponibili in window
Se il codice non webpack si basa su dipendenze disponibili come variabili in window
, utilizza i nomi delle dipendenze alias per i nomi delle variabili:
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
Con questa configurazione, webpack non raggruppa react
e react-dom
pacchetti. Verranno invece sostituiti con un testo simile al seguente:
// bundle.js (part of)
(function(module, exports) {
// A module that exports `window.React`. Without `externals`,
// this module would include the whole React bundle
module.exports = React;
}),
(function(module, exports) {
// A module that exports `window.ReactDOM`. Without `externals`,
// this module would include the whole ReactDOM bundle
module.exports = ReactDOM;
})
Se le dipendenze vengono caricate come pacchetti AMD
Se il codice non webpack non espone dipendenze in window
, le cose si complicano.
Tuttavia, puoi comunque evitare di caricare lo stesso codice due volte se il codice non webpack consuma queste
dipendenze come pacchetti AMD.
Per farlo, compila il codice webpack come bundle AMD e crea gli alias dei moduli per gli URL delle librerie:
// webpack.config.js
module.exports = {
output: {
libraryTarget: 'amd'
},
externals: {
'react': {
amd: '/libraries/react.min.js'
},
'react-dom': {
amd: '/libraries/react-dom.min.js'
}
}
};
Webpack aggrega il bundle in define()
e lo farà dipendere da questi URL:
// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
Se il codice non webpack utilizza gli stessi URL per caricare le sue dipendenze, questi file verranno caricati solo una volta. Altre richieste utilizzeranno la cache del caricatore.
Per approfondire
- Documenti Webpack su
externals
Riepilogo
- Attiva la modalità di produzione se utilizzi webpack 4
- Minimizza il codice con le opzioni di minimizzazione e caricamento a livello di bundle
- Rimuovi il codice solo per lo sviluppo sostituendo
NODE_ENV
conproduction
- Utilizzare i moduli ES per consentire l'eliminazione degli alberi
- Comprimi le immagini
- Applica ottimizzazioni specifiche delle dipendenze
- Abilita concatenazione dei moduli
- Usa
externals
se questo è adatto alle tue esigenze