In che modo webpack contribuisce alla memorizzazione nella cache degli asset
L'operazione successiva (dopo aver ottimizzato le dimensioni dell'app che migliora il tempo di caricamento dell'app è la memorizzazione nella cache. usalo per mantenere parti dell'app sul client ed evitare di scaricarle di nuovo ogni volta.
Usa il controllo delle versioni e le intestazioni cache del bundle
L'approccio comune per la memorizzazione nella cache è:
comunica al browser di memorizzare nella cache un file per molto tempo (ad esempio, un anno):
# Server header Cache-Control: max-age=31536000
Se non sai cosa fa
Cache-Control
, leggi l'eccellente post di Jake Archibald sulle best practice per la memorizzazione nella cache.e rinomina il file quando viene modificato per forzarne un 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 raggiungerà la rete solo se il nome del file cambia (o se è trascorso un anno).
Con il webpack si esegue la stessa operazione, ma invece di 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 client, utilizza HtmlWebpackPlugin
o
WebpackManifestPlugin
.
Il HtmlWebpackPlugin
è un
approccio semplice, ma meno flessibile. Durante la compilazione, il plug-in genera
un file HTML che include tutte le risorse compilate. Se la logica del tuo server non è complessa, allora dovrebbe essere sufficiente:
<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>
WebpackManifestPlugin
è un approccio più flessibile, utile se hai una parte complessa del server.
Durante la build, genera un file JSON con una mappatura tra i nomi dei file senza hash e i nomi dei file con hash. Usate 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 del codice effettivo dell'app. Se li sposti in un file separato, il browser sarà in grado di memorizzarli nella cache separatamente e non li scaricherà nuovamente ogni volta che il codice dell'app viene modificato.
Per estrarre le dipendenze in un blocco separato, esegui tre passaggi:
Sostituisci il nome del file di output con
[name].[chunkname].js
:// webpack.config.js module.exports = { output: { // Before filename: 'bundle.[chunkhash].js', // After filename: '[name].[chunkhash].js' } };
Quando il webpack crea l'app, sostituisce
[name]
con il nome di un blocco. Se non aggiungiamo la parte[name]
, dovremo differenziare i blocchi 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 blocco. Questo nome verrà sostituito al posto di
[name]
nel passaggio 1.A questo punto, se crei l'app, questo blocco includerà l'intero codice dell'app, proprio come non abbiamo fatto con questi passaggi. Ma la situazione cambierà tra un secondo.
In webpack 4, aggiungi l'opzione
optimization.splitChunks.chunks: 'all'
nella configurazione del webpack:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };
Questa opzione consente la suddivisione intelligente del codice. Con questo strumento, webpack estrae il codice del fornitore se supera i 30 kB (prima della minimizzazione e di gzip). Viene inoltre estratto il codice comune, utile se la tua build produce diversi bundle (ad esempio se suddividi l'app in route).
Nel 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 che includono i percorsi
node_modules
e li sposta in un file separato chiamatovendor.[chunkhash].js
.
Dopo queste modifiche, ogni build genererà due file invece di uno: main.[chunkhash].js
e
vendor.[chunkhash].js
(vendors~main.[chunkhash].js
per webpack 4). Nel caso del webpack 4,
il bundle del fornitore potrebbe non essere generato se le dipendenze sono piccole. È possibile farlo:
$ 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 questi file nella cache separatamente e scarica nuovamente solo il codice che cambia.
Codice runtime Webpack
Sfortunatamente, estrarre solo il codice del fornitore non è sufficiente. Se provi a modificare 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, a parte il codice dei moduli, ha un runtime, ovvero una piccola porzione di codice che gestisce l'esecuzione del modulo. Quando suddividi il codice in più file, questa porzione di codice inizia a includere una mappatura tra gli ID dei blocchi e i file corrispondenti:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack include questo runtime nell'ultimo blocco generato, che nel nostro caso è vendor
. Ogni volta che un blocco cambia, cambia anche questa porzione di codice,
causando la modifica dell'intero blocco vendor
.
Per risolvere questo problema, spostiamo il runtime in un file separato. In webpack 4, puoi farlo abilitando l'opzione optimization.runtimeChunk
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
Nel webpack 3, crea un blocco 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, per ogni build verranno generati 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
in ordine inverso. Hai terminato:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>
Per approfondire
- Guida Webpack sulla memorizzazione nella cache a lungo termine
- Documenti Webpack su runtime e file manifest webpack
- "Come ottenere il massimo dal CommonsChunkPlugin"
- Come funzionano
optimization.splitChunks
eoptimization.runtimeChunk
Runtime webpack in linea per salvare una richiesta HTTP aggiuntiva
Per migliorare ulteriormente, prova a incorporare il runtime webpack nella risposta HTML. Ad esempio, al posto di questo:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
Segui questi passaggi:
<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>
Il runtime è di dimensioni ridotte e se lo integra puoi salvare una richiesta HTTP (presto molto importante con HTTP/1; meno importante con HTTP/2, ma che potrebbe avere un effetto).
Ecco come fare.
Se generi il codice HTML con il componente HTMLWebpackPlugin
Se utilizzi HtmlWebpackPlugin per generare un file HTML, InlineSourcePlugin ti serve solo:
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 server personalizzata
Con webpack 4:
Aggiungi
WebpackManifestPlugin
per conoscere il nome generato del blocco di runtime:// webpack.config.js (for webpack 4) const ManifestPlugin = require('webpack-manifest-plugin'); module.exports = { plugins: [ new ManifestPlugin() ] };
Una build con questo plug-in crea un file simile al seguente:
// manifest.json { "runtime~main.js": "runtime~main.8e0d62a03.js" }
Incorpora in modo pratico i contenuti del blocco di runtime. 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> … `); });
O con il webpack 3:
Rendi statico il nome del runtime specificando
filename
:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
Incorpora i contenuti
runtime.js
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 di caricamento lento che non ti serve al momento
A volte una pagina ha parti più e meno importanti:
- Se carichi la pagina di un video su YouTube, ti interessano 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 solo gli elementi più importanti per primi e caricando lentamente le parti rimanenti in un secondo momento. Utilizza la funzione import()
e la suddivisione del codice per:
// 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()
indica che vuoi caricare un modulo specifico in modo dinamico. Quando
il webpack rileva import('./module.js')
, sposta questo modulo in un
blocco 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 la scarica solo quando l'esecuzione raggiunge la funzione import()
.
Questa operazione ridurrà le dimensioni del bundle main
, migliorando il tempo di caricamento iniziale.
Inoltre, migliorerà la memorizzazione nella cache: se modifichi il codice nel blocco principale, il blocco dei commenti non verrà modificato.
Per approfondire
- Documenti Webpack per la
funzione
import()
- La proposta JavaScript per l'implementazione della sintassi
import()
Suddividi il codice in route e pagine
Se la tua app ha più route o pagine, ma è presente un solo file JS con il codice (un unico blocco main
), è probabile che vengano forniti byte in più per ogni richiesta. Ad esempio, quando un utente visita una home page del tuo sito:
non è necessario caricare il codice per visualizzare un articolo che si trova in un'altra pagina, ma lo caricheranno. Inoltre, se l'utente visita sempre solo la home page e modifichi il codice dell'articolo, il webpack renderà non valido l'intero bundle e l'utente dovrà scaricare nuovamente l'intera app.
Se l'app viene suddivisa in pagine (o route, se si tratta di un'app a pagina singola), l'utente scarica solo il codice pertinente. Inoltre, il browser memorizzerà meglio il codice dell'app: se modifichi il codice della home page, il webpack renderà non valido solo il blocco corrispondente.
Per app con una sola pagina
Per suddividere le app con una sola pagina in base alle route, utilizza import()
(consulta la sezione "Codice per il caricamento lento che non ti serve al momento"). Se usi un framework,
potrebbe esistere una soluzione esistente per questo:
- "Suddivisione del codice"
nei documenti di
react-router
(per React) - "Lazy Load
Routes" nei documenti di
vue-router
(per Vue.js)
Per le app con più pagine tradizionali
Per suddividere le app tradizionali per pagine, utilizza i punti di ingresso del webpack. Se la tua app ha tre tipi di pagine (home page, pagina dell'articolo e 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 voce, 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 presentano degli svantaggi. Se due punti di ingresso utilizzano Lodash e non hai spostato le dipendenze in un bundle di fornitori, entrambi i punti di ingresso includeranno una copia di Lodash. Per risolvere questo problema, nel webpack 4 aggiungi l'opzione optimization.splitChunks.chunks: 'all'
alla configurazione del webpack:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Questa opzione consente la suddivisione intelligente del codice. Con questa opzione, il Webpack cerca il codice comune e lo estrae in file separati.
In alternativa, nel webpack 3, usa l'icona CommonsChunkPlugin
, che sposterà le dipendenze comuni in un nuovo file specificato:
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
minChunks: 2 // 2 is the default value
})
]
};
Gioca con il valore minChunks
per trovare il migliore. In genere, è consigliabile mantenerlo basso, ma aumentare se il numero di blocchi cresce. Ad esempio, per 3 blocchi, minChunks
potrebbe essere 2, ma per 30 blocchi potrebbe essere 8.
Perché se mantieni il valore su 2, troppi moduli entreranno nel file comune, aumentandolo di conseguenza.
Per approfondire
- Documenti Webpack sul concetto di entry point
- Documenti webpack relativi al CommonsChunkPlugin
- "Come ottenere il massimo dal CommonsChunkPlugin"
- Come funzionano
optimization.splitChunks
eoptimization.runtimeChunk
Rendi più stabili gli ID modulo
Quando crea il codice, il webpack assegna un ID a ogni modulo. In seguito, questi ID verranno
utilizzati nei require()
all'interno del bundle. Di solito, gli ID vengono visualizzati nell'output della build
subito prima dei percorsi del modulo:
$ 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 (ovvero il primo modulo ha ID 0, il secondo ha ID 1 e così via). Il problema è che, quando aggiungi un nuovo modulo, questo potrebbe apparire al centro dell'elenco di moduli, modificando gli ID di tutti i 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]
↓ E guarda cosa è successo! 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, tutti i blocchi che includono o dipendono da moduli con ID modificati vengono annullati, anche se il codice effettivo non è cambiato. Nel nostro caso, il blocco 0
(il blocco con comments.js
) e il blocco main
(il blocco con l'altro codice dell'app) vengono invalidati, mentre solo il blocco main
avrebbe dovuto essere.
Per risolvere questo problema, modifica il modo in cui vengono calcolati gli ID modulo utilizzando
HashedModuleIdsPlugin
.
Sostituisce gli ID basati su contatore 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 rinomini o sposti il modulo. I nuovi moduli non avranno effetto sugli ID di altri moduli.
Per attivare il plug-in, aggiungilo alla sezione plugins
del file di configurazione:
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin()
]
};
Per approfondire
- Documentazione sul webpack relativa a HashedModuleIdsPlugin
Riepilogo
- Memorizza nella cache il bundle e differenzia le versioni modificandone il nome
- Suddividi il bundle in codice dell'app, codice del fornitore e runtime
- Incorporare il runtime per salvare una richiesta HTTP
- Esegui il caricamento lento del codice non critico con
import
- Suddividi il codice per route/pagine per evitare di caricare contenuti superflui