In che modo webpack è utile per la memorizzazione nella cache degli asset
Dopo aver ottimizzato le dimensioni dell'app, la prossima cosa che migliora il tempo di caricamento dell'app è la memorizzazione nella cache. Utilizzalo per conservare parti dell'app sul client ed evitare di scaricarle di nuovo ogni volta.
Utilizzare il controllo della versione del bundle e le intestazioni della cache
L'approccio comune alla memorizzazione nella cache è:
di memorizzare nella cache un file per un periodo di tempo molto lungo (ad es. un anno):
# Server header Cache-Control: max-age=31536000
Se non sai cosa fa
Cache-Control
, consulta l'eccellente post di Jake Archibald sulle best practice per la memorizzazione nella cache.e rinomina il file quando viene modificato per forzare il nuovo download:
<!-- Before the change --> <script src="./index-v15.js"></script> <!-- After the change --> <script src="./index-v16.js"></script>
Questo approccio indica al browser di scaricare il file JS, memorizzarlo nella cache e utilizzare la copia memorizzata nella cache. Il browser accederà alla rete solo se il nome del file cambia (o se passa un anno).
Con webpack, fai lo stesso, ma anziché un numero di versione, specifichi l'hash del file. Per includere l'hash nel nome del file, utilizza
[chunkhash]
:
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
}
};
Se hai bisogno del nome del file per inviarlo al cliente, utilizza HtmlWebpackPlugin
o WebpackManifestPlugin
.
HtmlWebpackPlugin
è un approccio semplice, ma meno flessibile. Durante la compilazione, questo plug-in genera un
file HTML che include tutte le risorse compilate. Se la logica del server non è complessa, dovrebbe essere sufficiente:
<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>
WebpackManifestPlugin
è un approccio più flessibile, utile se hai una parte del server complessa.
Durante la compilazione, viene generato un file JSON con una mappatura tra i nomi dei file senza hash e i nomi dei file con hash. Utilizza questo JSON sul server per scoprire con quale file lavorare:
// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}
Per approfondire
- Jake Archibald sulle best practice per la memorizzazione nella cache
Estrai le dipendenze e il runtime in un file separato
Dipendenze
Le dipendenze dell'app tendono a cambiare meno spesso rispetto al codice dell'app effettivo. Se li sposti in un file separato, il browser potrà memorizzarli nella cache separatamente e non li scaricherà di nuovo ogni volta che il codice dell'app cambia.
Per estrarre le dipendenze in un chunk separato, svolgi tre passaggi:
Sostituisci il nome file di output con
[name].[chunkname].js
:// webpack.config.js module.exports = { output: { // Before filename: 'bundle.[chunkhash].js', // After filename: '[name].[chunkhash].js' } };
Quando webpack compila l'app, sostituisce
[name]
con il nome di uno chunk. Se non aggiungiamo la parte[name]
, dovremo differenziare i chunk in base al loro hash, il che è piuttosto difficile.Converti il campo
entry
in un oggetto:// webpack.config.js module.exports = { // Before entry: './index.js', // After entry: { main: './index.js' } };
In questo snippet, "main" è il nome di un chunk. Questo nome verrà sostituito al posto di
[name]
del passaggio 1.A questo punto, se crei l'app, questo blocco includerà l'intero codice dell'app, come se non avessimo eseguito questi passaggi. Ma tra un attimo cambierà.
In webpack 4, aggiungi l'opzione
optimization.splitChunks.chunks: 'all'
alla configurazione di webpack:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };
Questa opzione consente la suddivisione del codice in modo intelligente. Con questo, webpack estrae il codice del fornitore se supera i 30 kB (prima della minimizzazione e di gzip). Verrà estratto anche il codice comune, utile se la compilazione produce più bundle (ad es. se dividi l'app in route).
In webpack 3, aggiungi
CommonsChunkPlugin
:// webpack.config.js (for webpack 3) module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ // A name of the chunk that will include the dependencies. // This name is substituted in place of [name] from step 1 name: 'vendor', // A function that determines which modules to include into this chunk minChunks: module => module.context && module.context.includes('node_modules'), }) ] };
Questo plug-in prende tutti i moduli i cui percorsi includono
node_modules
e li sposta in un file separato chiamatovendor.[chunkhash].js
.
Dopo queste modifiche, ogni compilazione genererà due file anziché uno: main.[chunkhash].js
e
vendor.[chunkhash].js
(vendors~main.[chunkhash].js
per webpack 4). Nel caso di webpack 4, il bundle del fornitore potrebbe non essere generato se le dipendenze sono ridotte e non c'è problema:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
Il browser memorizza in cache questi file separatamente e scarica di nuovo solo il codice che cambia.
Codice di runtime di Webpack
Purtroppo, non è sufficiente estrarre solo il codice fornitore. Se provi a cambiare qualcosa nel codice dell'app:
// index.js
…
…
// E.g. add this:
console.log('Wat');
noterai che anche l'hash vendor
cambia:
Asset Size Chunks Chunk Names
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
↓
Asset Size Chunks Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js 47 kB 1 [emitted] vendor
Questo accade perché il bundle webpack, oltre al codice dei moduli, ha un ambiente di runtime, ovvero un piccolo frammento di codice che gestisce l'esecuzione del modulo. Quando dividi il codice in più file, questo frammento di codice inizia a includere una mappatura tra gli ID chunk e i file corrispondenti:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack include questo runtime nell'ultimo chunk generato, ovvero vendor
nel nostro caso. E ogni volta che un chunk cambia, cambia anche questo pezzo di codice, provocando la modifica dell'intero chunk vendor
.
Per risolvere il problema, spostiamo il runtime in un file separato. In webpack 4,questo viene ottenuto attivando l'opzione optimization.runtimeChunk
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
In webpack 3,crea un chunk vuoto aggiuntivo con CommonsChunkPlugin
:
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: module => module.context && module.context.includes('node_modules')
}),
// This plugin must come after the vendor one (because webpack
// includes runtime into the last chunk)
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
// minChunks: Infinity means that no app modules
// will be included into this chunk
minChunks: Infinity
})
]
};
Dopo queste modifiche, ogni build genererà tre file:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 1 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
Includili in index.html
nell'ordine inverso e il gioco è fatto:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>
Per approfondire
- Guida di Webpack sulla memorizzazione nella cache a lungo termine
- Documentazione di Webpack sul runtime e sul manifest di webpack
- "Ottenere il massimo da CommonsChunkPlugin"
- Come funzionano
optimization.splitChunks
eoptimization.runtimeChunk
Esegui il runtime webpack in linea per risparmiare una richiesta HTTP aggiuntiva
Per migliorare ulteriormente la situazione, prova a eseguire l'inlining del runtime webpack nella risposta HTML. Ad esempio, invece di:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
procedi nel seguente modo:
<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>
Il runtime è piccolo e l'inserimento in linea ti consente di risparmiare una richiesta HTTP (molto importante con HTTP/1; meno importante con HTTP/2, ma potrebbe comunque avere un effetto).
Ecco come fare.
Se generi HTML con HtmlWebpackPlugin
Se utilizzi il plug-in HtmlWebpackPlugin per generare un file HTML, il plug-in InlineSourcePlugin è tutto ciò che ti serve:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
inlineSource: 'runtime~.+\\.js',
}),
new InlineSourcePlugin()
]
};
Se generi HTML utilizzando una logica del server personalizzata
Con webpack 4:
Aggiungi
WebpackManifestPlugin
per conoscere il nome generato del chunk di runtime:// webpack.config.js (for webpack 4) const ManifestPlugin = require('webpack-manifest-plugin'); module.exports = { plugins: [ new ManifestPlugin() ] };
Una compilazione con questo plug-in crea un file simile al seguente:
// manifest.json { "runtime~main.js": "runtime~main.8e0d62a03.js" }
Inserisci in linea i contenuti del chunk di runtime in modo pratico. Ad esempio, con Node.js ed Express:
// server.js const fs = require('fs'); const manifest = require('./manifest.json'); const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8'); app.get('/', (req, res) => { res.send(` … <script>${runtimeContent}</script> … `); });
Oppure con webpack 3:
Rendi statico il nome del runtime specificando
filename
:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
Inserisci i contenuti
runtime.js
in linea in modo pratico. Ad esempio, con Node.js ed Express:// server.js const fs = require('fs'); const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8'); app.get('/', (req, res) => { res.send(` … <script>${runtimeContent}</script> … `); });
Codice con caricamento differito che non ti serve al momento
A volte, una pagina ha parti più o meno importanti:
- Se carichi la pagina di un video su YouTube, ti interessa di più il video che i commenti. In questo caso, il video è più importante dei commenti.
- Se apri un articolo su un sito di notizie, ti interessa di più il testo dell'articolo che gli annunci. In questo caso, il testo è più importante degli annunci.
In questi casi, migliora le prestazioni di caricamento iniziale scaricando prima solo gli elementi più importanti e caricando in modo lazy le parti rimanenti in un secondo momento. A tale scopo, utilizza la funzione import()
e la suddivisione del codice:
// videoPlayer.js
export function renderVideoPlayer() { … }
// comments.js
export function renderComments() { … }
// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();
// …Custom event listener
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});
import()
specifica che vuoi caricare un modulo specifico in modo dinamico. Quando webpack vede import('./module.js')
, sposta questo modulo in un frammento separato:
$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.f7e53d8e13e9a2745d6d.js 60 kB 1 [emitted] main
./vendor.4f14b6326a80f4752a98.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
e lo scarica solo quando l'esecuzione raggiunge la funzione import()
.
In questo modo, il bundle main
sarà più piccolo e il tempo di caricamento iniziale migliorerà.
Inoltre, migliorerà la memorizzazione nella cache: se modifichi il codice nel chunk principale, il chunk dei commenti non verrà interessato.
Per approfondire
- Documentazione di Webpack per la funzione
import()
- La proposta di JavaScript per l'implementazione della sintassi
import()
Suddividi il codice in route e pagine
Se la tua app ha più route o pagine, ma esiste un solo file JS con il codice (un singolo chunk main
), è probabile che tu stia pubblicando byte aggiuntivi su ogni richiesta. Ad esempio, quando un utente visita la home page del tuo sito:
non devono caricare il codice per il rendering di un articolo che si trova su un'altra pagina, ma lo faranno. Inoltre, se l'utente visita sempre solo la home page e apporti una modifica al codice dell'articolo, webpack invaliderà l'intero bundle e l'utente dovrà scaricare di nuovo l'intera app.
Se dividiamo l'app in pagine (o route, se si tratta di un'app a pagina singola), l'utente scarica solo il codice pertinente. Inoltre, il browser memorizza meglio nella cache il codice dell'app: se modifichi il codice della home page, webpack invaliderà solo il corrispondente frammento.
Per le app a pagina singola
Per suddividere le app a pagina singola in base ai percorsi, utilizza import()
(consulta la sezione "Codice di caricamento differito
che non ti serve al momento"). Se utilizzi un framework, potrebbe avere già una soluzione per questo problema:
- "Suddivisione del codice"
nella documentazione di
react-router
(per React) - "Route con caricamento lento" nella documentazione di
vue-router
(per Vue.js)
Per le app tradizionali con più pagine
Per suddividere le app tradizionali per pagine, utilizza i punti di entry di webpack. Se la tua app ha tre tipi di pagine: la home page, la pagina dell'articolo e la pagina dell'account utente, deve avere tre voci:
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
}
};
Per ogni file di entrata, webpack creerà un albero delle dipendenze separato e genererà un bundle che include solo i moduli utilizzati da quella voce:
$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime
Pertanto, se solo la pagina dell'articolo utilizza Lodash, i bundle home
e profile
non lo includeranno e l'utente non dovrà scaricare questa libreria quando visita la home page.
Tuttavia, gli alberi delle dipendenze separati hanno i loro svantaggi. Se due punti di contatto utilizzano Lodash e non hai spostato le dipendenze in un bundle del fornitore, entrambi i punti di contatto includeranno una copia di Lodash. Per risolvere il problema, in webpack 4,aggiungi l'opzione optimization.splitChunks.chunks: 'all'
alla configurazione di webpack:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Questa opzione consente la suddivisione del codice in modo intelligente. Con questa opzione, webpack cercherà automaticamente il codice comune e lo estrarrà in file separati.
In alternativa, in webpack 3,utilizza CommonsChunkPlugin
per spostare le dipendenze comuni in un nuovo file specificato:
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
minChunks: 2 // 2 is the default value
})
]
};
Non esitare a modificare il valore minChunks
per trovare quello migliore. In genere,
è preferibile mantenere il valore ridotto, ma aumentarlo se il numero di chunk aumenta. Ad esempio, per 3 chunk, minChunks
potrebbe essere 2, ma per 30 chunk potrebbe essere 8, perché se mantieni il valore 2, nel file comune verranno inseriti troppi moduli, gonfiandolo troppo.
Per approfondire
- Documentazione di Webpack sul concetto di punti di entry
- Documentazione di Webpack sul plug-in CommonsChunkPlugin
- "Ottenere il massimo da CommonsChunkPlugin"
- Come funzionano
optimization.splitChunks
eoptimization.runtimeChunk
Rendere gli ID modulo più stabili
Durante la compilazione del codice, webpack assegna a ogni modulo un ID. In seguito, questi ID vengono utilizzati nei require()
all'interno del bundle. In genere, gli ID vengono visualizzati nell'output della compilazione
appena prima dei percorsi dei moduli:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
↓ Qui
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module
Per impostazione predefinita, gli ID vengono calcolati utilizzando un contatore (ad es. il primo modulo ha l'ID 0, il secondo l'ID 1 e così via). Il problema è che quando aggiungi un nuovo modulo, questo potrebbe apparire nel mezzo dell'elenco dei moduli, cambiando tutti gli ID dei moduli successivi:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
↓ Abbiamo aggiunto un nuovo modulo…
[4] ./webPlayer.js 24 kB {1} [built]
↓ Guarda cosa ha fatto. comments.js
ora ha l'ID 5 anziché 4
[5] ./comments.js 58 kB {0} [built]
↓ ads.js
ora ha l'ID 6 anziché 5
[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module
In questo modo, vengono invalidati tutti i chunk che includono o dipendono da moduli con ID modificati, anche se il codice effettivo non è cambiato. Nel nostro caso, il chunk 0
(il chunk con comments.js
) e il chunk main
(il chunk con l'altro codice dell'app) vengono invalidati, mentre solo il chunk main
avrebbe dovuto esserlo.
Per risolvere il problema, modifica il modo in cui vengono calcolati gli ID modulo utilizzando
HashedModuleIdsPlugin
.
Sostituisce gli ID basati su contatori con hash dei percorsi dei moduli:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime
↓ Qui
[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module
Con questo approccio, l'ID di un modulo cambia solo se lo rinomini o lo sposti. I nuovi moduli non influiranno sugli ID di altri moduli.
Per attivare il plug-in, aggiungilo alla sezione plugins
della configurazione:
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin()
]
};
Per approfondire
- Documentazione di Webpack sul plug-in HashedModuleIdsPlugin
Riepilogo
- Memorizza nella cache il bundle e distingui le versioni modificando il nome del bundle
- Suddividi il bundle in codice dell'app, codice del fornitore e runtime
- Inserire in linea il runtime per salvare una richiesta HTTP
- Carica in modo lazy il codice non critico con
import
- Suddividi il codice per percorsi/pagine per evitare di caricare elementi non necessari