Webpack modulių bundleris: optimizavimas produkcijai

Kodėl Webpack optimizavimas nėra tik „nice to have”

Kai pirmą kartą susiduri su Webpack, viskas atrodo paprasta – sukonfigūruoji keletą eilučių, paleidžiui `npm run build` ir voila, turime bundle’ą. Bet kai projektas pradeda augti, o klientas skundžiasi, kad puslapis kraunasi lėčiau nei Windows 95, supranti, kad reikia gilintis.

Webpack optimizavimas produkcijai nėra kažkoks papildomas „nice to have” dalykas, kurį gali atidėti vėlesniam laikui. Tai kritinis aspektas, kuris tiesiogiai veikia vartotojo patirtį, SEO rezultatus ir galiausiai – verslo rodiklius. Pagalvokite: kiekviena papildoma sekundė puslapio įkėlimo metu sumažina konversijas apie 7%. Tai nėra juokai.

Problema ta, kad Webpack iš dėžės nėra optimizuotas maksimaliam našumui. Jis suteikia jums įrankius, bet kaip jais naudotis – jūsų reikalas. Ir čia prasideda tikrasis žaidimas.

Production mode ir ką jis iš tiesų daro

Pirmas ir paprasčiausias žingsnis – įsitikinti, kad naudojate `mode: ‘production’`. Skamba akivaizdžiai, bet nustebsite, kiek projektų veikia su development mode arba visai be jo.

„`javascript
module.exports = {
mode: ‘production’,
// kita konfigūracija
};
„`

Kai nustatote production režimą, Webpack automatiškai aktyvuoja keletą optimizacijų: TerserPlugin minifikacijai, scope hoisting, tree shaking ir kitus dalykus. Bet tai tik pradžia. Daugelis developerių sustoja čia ir galvoja, kad viskas atlikta. Realybė tokia, kad tai tik paviršius.

Production mode neįjungia agresyviausių optimizacijų, nes jos gali pailginti build laiką arba retais atvejais sukelti problemų. Todėl reikia rankiniu būdu konfigūruoti papildomus dalykus.

Code splitting strategija, kuri veikia realiame gyvenime

Code splitting yra viena iš galingiausių Webpack funkcijų, bet dažnai ji būna blogai suprantama ir neteisingai naudojama. Esmė paprasta – vietoj vieno didžiulio bundle’o, kuris sveria 2MB, sukuriate kelis mažesnius, kurie kraunami pagal poreikį.

Yra trys pagrindiniai code splitting būdai:

**Entry points splitting** – paprasčiausias, bet ne pats efektyviausias būdas. Tiesiog apibrėžiate kelis entry points:

„`javascript
module.exports = {
entry: {
main: ‘./src/index.js’,
admin: ‘./src/admin.js’,
},
};
„`

Problema ta, kad jei abu entry points naudoja tuos pačius modulius (pvz., React), jie bus įtraukti į abu bundle’us. Dubliavimas – ne tai, ko norime.

**Dynamic imports** – daug protingesnis būdas. Naudojate `import()` funkciją, kuri grąžina Promise:

„`javascript
button.addEventListener(‘click’, () => {
import(‘./heavyModule.js’)
.then(module => {
module.doSomething();
});
});
„`

Webpack automatiškai sukurs atskirą chunk’ą tam moduliui. Tai puikiai tinka modalams, retai naudojamoms funkcijoms, admin panelėms ir pan.

**SplitChunksPlugin** – čia prasideda tikroji magija. Šis plugin’as leidžia automatiškai atskirti bendrus modulius į atskirus chunk’us:

„`javascript
optimization: {
splitChunks: {
chunks: ‘all’,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: ‘vendors’,
priority: 10,
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
}
„`

Ši konfigūracija atskiria visus node_modules į atskirą vendor bundle’ą, o kodus, kurie naudojami bent dviejose vietose, į common bundle’ą. Kodėl tai svarbu? Nes vendor kodas keičiasi retai, todėl browser’is gali jį cache’inti ilgam laikui.

Tree shaking ir kodėl jūsų bundle’as vis tiek per didelis

Tree shaking – tai procesas, kuris pašalina nenaudojamą kodą iš bundle’o. Skamba puikiai, bet praktikoje dažnai neveikia taip, kaip tikitės.

Pirma, tree shaking veikia tik su ES6 moduliais (import/export). Jei naudojate CommonJS (require/module.exports), tree shaking neveiks. Tai reiškia, kad turite būti atsargūs su bibliotekomis, kurios vis dar naudoja senąjį formatą.

Antra, daugelis bibliotekų nėra parašytos taip, kad tree shaking veiktų efektyviai. Pavyzdžiui, jei importuojate:

„`javascript
import { debounce } from ‘lodash’;
„`

Vis tiek gaunate visą lodash biblioteką bundle’e. Sprendimas:

„`javascript
import debounce from ‘lodash/debounce’;
// arba
import debounce from ‘lodash-es/debounce’;
„`

Trečia, side effects. Jei modulis turi side effects (pvz., modifikuoja globalius objektus), Webpack negali jo saugiai pašalinti, net jei jis nenaudojamas. Todėl `package.json` turite nurodyti:

„`json
{
„sideEffects”: false
}
„`

Arba, jei kai kurie failai turi side effects (pvz., CSS):

„`json
{
„sideEffects”: [„*.css”, „*.scss”]
}
„`

Minifikacija ir kompresija: ne tik apie Terser

Webpack 5 naudoja Terser minifikacijai pagal nutylėjimą, bet galite jį sukonfigūruoti agresyvesniam veikimui:

„`javascript
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: [‘console.log’],
},
format: {
comments: false,
},
},
extractComments: false,
}),
],
}
„`

Ši konfigūracija pašalina visus console.log’us ir komentarus iš production build’o. Bet minifikacija – tai tik pusė istorijos.

CSS taip pat reikia minifikuoti. Jei naudojate MiniCssExtractPlugin, pridėkite CssMinimizerPlugin:

„`javascript
const CssMinimizerPlugin = require(‘css-minimizer-webpack-plugin’);

optimization: {
minimizer: [
`…`, // išlaiko default minimizers
new CssMinimizerPlugin(),
],
}
„`

Bet tikrasis game changer yra kompresija server’io lygmenyje. Gzip arba Brotli gali sumažinti bundle’o dydį 70-80%. Galite naudoti CompressionWebpackPlugin, kad iš anksto sukompresavus failus:

„`javascript
const CompressionPlugin = require(‘compression-webpack-plugin’);

plugins: [
new CompressionPlugin({
filename: ‘[path][base].gz’,
algorithm: ‘gzip’,
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
}),
new CompressionPlugin({
filename: ‘[path][base].br’,
algorithm: ‘brotliCompress’,
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
}),
]
„`

Cache’inimas ir ilgalaikis asset’ų saugojimas

Vienas didžiausių Webpack privalumų – galimybė generuoti failus su content hash’ais pavadinime. Tai leidžia browser’iui cache’inti failus be galo ilgai, nes kai turinys pasikeičia, pasikeičia ir failo pavadinimas.

„`javascript
output: {
filename: ‘[name].[contenthash].js’,
chunkFilename: ‘[name].[contenthash].chunk.js’,
clean: true, // išvalo output direktoriją prieš build’ą
}
„`

Bet yra viena problema – runtime chunk’as. Webpack runtime kodas, kuris valdo modulių įkėlimą, keičiasi kiekvieną kartą, kai pasikeičia bet kuris modulis. Tai reiškia, kad jūsų pagrindinis bundle’as invaliduoja cache’ą net jei pasikeitė tik vienas mažas modulis.

Sprendimas – iškelti runtime į atskirą failą:

„`javascript
optimization: {
runtimeChunk: ‘single’,
moduleIds: ‘deterministic’,
}
„`

`moduleIds: ‘deterministic’` užtikrina, kad modulių ID būtų stabilūs tarp build’ų, todėl vendor bundle’as nesikeičia, jei nepasikeitė jo turinys.

Dar vienas svarbus dalykas – CSS failų hash’avimas:

„`javascript
const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’);

plugins: [
new MiniCssExtractPlugin({
filename: ‘[name].[contenthash].css’,
chunkFilename: ‘[id].[contenthash].css’,
}),
]
„`

Source maps produkcijai: taip ar ne?

Tai amžinas klausimas. Source maps leidžia debug’inti production kodą, bet jie padidina bundle’o dydį ir gali atskleisti jūsų šaltinio kodą.

Kompromisinis sprendimas – naudoti `hidden-source-map` arba `nosources-source-map`:

„`javascript
devtool: ‘hidden-source-map’,
„`

`hidden-source-map` generuoja source maps, bet neprideda reference į juos bundle’e. Tai reiškia, kad vartotojai jų nematys, bet jūs galite juos naudoti error tracking sistemose (Sentry, Rollbar ir pan.).

`nosources-source-map` sukuria source maps be originalaus šaltinio kodo, tik su stack trace’ais. Tai saugiau, bet mažiau naudinga debug’inimui.

Jei jūsų projektas nėra super slaptas, rekomenduočiau `source-map` development’e ir `hidden-source-map` production’e. Taip turėsite geriausią debug’inimo patirtį neaukodami saugumo.

Analizė ir matavimas: ką iš tiesų optimizuojate

Negalite optimizuoti to, ko nematote. Prieš pradėdami bet kokias optimizacijas, turite suprasti, kas sudaro jūsų bundle’ą.

**Webpack Bundle Analyzer** – būtinas įrankis:

„`javascript
const BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPlugin;

plugins: [
new BundleAnalyzerPlugin({
analyzerMode: ‘static’,
openAnalyzer: false,
reportFilename: ‘bundle-report.html’,
}),
]
„`

Tai sukurs interaktyvią vizualizaciją, kur matysite, kas užima daugiausiai vietos. Dažniausiai rasite kelis nustebinančius dalykus: moment.js su visomis locale’ėmis (600KB!), lodash vietoj lodash-es, arba kažkokią biblioteką, kurią importavote vieną kartą testavimui ir pamiršote pašalinti.

Kitas svarbus įrankis – `speed-measure-webpack-plugin`, kuris parodo, kiek laiko užtrunka kiekvienas loader ir plugin:

„`javascript
const SpeedMeasurePlugin = require(‘speed-measure-webpack-plugin’);
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
// jūsų webpack config
});
„`

Tai padės identifikuoti, kurie loader’iai lėtina build’ą. Galbūt babel-loader kompiliuoja per daug failų? Galbūt image loader’is procesiną gigantiškas nuotraukas?

Kai viskas sukonfigūruota, bet vis dar lėta

Kartais padarote viską teisingai, bet rezultatas vis tiek nėra patenkinamas. Štai keletas papildomų triukų:

**Lazy loading viskam, kas ne critical path**. Jei turite carousel’ę, modal’ą, chart’ą – lazy load’inkite juos. React.lazy ir Suspense čia jūsų draugai:

„`javascript
const HeavyChart = React.lazy(() => import(‘./HeavyChart’));

function Dashboard() {
return (
Kraunasi…

}>


);
}
„`

**Preload ir prefetch**. Webpack palaiko magic comments, kurie leidžia kontroliuoti, kaip chunk’ai kraunami:

„`javascript
import(/* webpackPreload: true */ ‘./CriticalComponent’);
import(/* webpackPrefetch: true */ ‘./MaybeNeededLater’);
„`

Preload kraunasi lygiagrečiai su parent chunk’u, prefetch – kai browser’is idle’ina.

**Externals**. Jei naudojate CDN, galite iškelti sunkias bibliotekas iš bundle’o:

„`javascript
externals: {
react: ‘React’,
‘react-dom’: ‘ReactDOM’,
}
„`

Tada HTML’e:

„`html


„`

Bet būkite atsargūs – tai prideda papildomų network request’ų ir gali sukelti problemų su versijomis.

**Module federation** (Webpack 5). Jei turite micro-frontend’ų architektūrą, tai game changer. Leidžia dalintis moduliais tarp skirtingų aplikacijų runtime’e:

„`javascript
new ModuleFederationPlugin({
name: ‘app1’,
filename: ‘remoteEntry.js’,
exposes: {
‘./Button’: ‘./src/Button’,
},
shared: [‘react’, ‘react-dom’],
})
„`

Tai pažangi tema, bet jei jūsų projektas yra pakankamai didelis, verta išsiaiškinti.

Kai optimizacija tampa obsesija (ir kodėl tai gerai)

Webpack optimizavimas niekada nebūna „baigtas”. Visada yra dar vienas kilobyte’as, kurį galima nuskusti, dar vienas request’as, kurį galima išvengti, dar viena sekundė, kurią galima sutaupyti.

Bet svarbu suprasti prioritetus. Jei jūsų bundle’as 50KB, nereikia praleisti savaitės bandant jį sumažinti iki 45KB. Bet jei jis 2MB, tai kritinė problema, kurią reikia spręsti dabar.

Pradėkite nuo low-hanging fruit: production mode, code splitting, tree shaking, minifikacija. Tai duos didžiausią efektą mažiausiomis pastangomis. Tada eikite giliau – analizuokite bundle’ą, optimizuokite images, lazy load’inkite komponentus.

Ir svarbiausia – matuokite. Naudokite Lighthouse, WebPageTest, Chrome DevTools. Žiūrėkite ne tik bundle’o dydį, bet ir Time to Interactive, First Contentful Paint, Largest Contentful Paint. Tai metrikų, kurios iš tiesų rūpi vartotojams ir Google.

Webpack gali atrodyti sudėtingas, kartais net frustruojantis. Bet kai suprantate, kaip jis veikia, ir mokate jį optimizuoti, jis tampa neįtikėtinai galingas įrankis. Jūsų vartotojai galbūt niekada nesužinos, kiek pastangų įdėjote į tų kelių sekundžių sutaupymą. Bet jie pajus skirtumą, ir tai – vienintelis dalykas, kuris iš tiesų svarbu.

Daugiau

Python dataclasses: struktūrizuoti duomenys