Kodėl bundling’as vis dar svarbus 2025-aisiais
Galite paklausti – ar vis dar reikia galvoti apie bundling’ą, kai turime HTTP/2, HTTP/3, o naršyklės jau palaiko ES modulius? Atsakymas paprastas: taip, reikia. Nors technologijos tobulėja, realybė yra tokia, kad dauguma projektų vis dar gauna didžiulę naudą iš gerai sukonfigūruoto bundler’io.
Webpack’as nėra naujiena – jis su mumis jau beveik dešimtmetį. Tačiau būtent dėl to jis ir yra toks galingas. Ekosistema išsivystė iki tokio lygio, kad galite optimizuoti beveik bet ką. Problema ta, kad daugelis kūrėjų naudoja default konfigūraciją arba kažkada nukopijuotą setup’ą iš Stack Overflow, o paskui stebi, kaip jų aplikacija kraunasi 5 sekundes.
Dirbau projekte, kur production bundle’as svėrė 8MB. Aštuoni megabaitai JavaScript’o! Po kelių dienų optimizavimo pavyko sumažinti iki 800KB, o su gzip – iki 250KB. Tai ne magiška formulė, o tiesiog sistemingas požiūris ir supratimas, kaip Webpack veikia po gaubtu.
Analizuojam, ką iš tikrųjų bundle’inam
Pirmas žingsnis visada turėtų būti analizė. Negalite optimizuoti to, ko nematote. Webpack Bundle Analyzer yra jūsų geriausias draugas čia. Įdiekite jį ir paruoškitės nustebti:
npm install --save-dev webpack-bundle-analyzer
Konfigūracijoje:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: ‘static’,
openAnalyzer: false,
reportFilename: ‘bundle-report.html’
})
]
};
Kai paleisite build’ą, gausite interaktyvią vizualizaciją, kur matysite kiekvieno modulio dydį. Dažniausiai čia ir prasideda „aha” momentai. Pavyzdžiui, pastebite, kad moment.js su visomis lokalėmis užima 200KB, nors jums reikia tik vienos. Arba kad lodash importuojate visą biblioteką, kai naudojate tik 3 funkcijas.
Viename projekte radau, kad bundle’e buvo net 4 skirtingos React versijos. Kaip taip nutiko? Skirtingos priklausomybės naudojo skirtingas versijas, o package manager’is jas visas įdiegė. Tokius dalykus sunku pastebėti be tinkamų įrankių.
Code splitting strategija, kuri veikia
Code splitting skamba kaip paprasta koncepcija – padalink kodą į mažesnius gabalus. Bet praktikoje reikia strategijos, ne tik atsitiktinio dalijimo.
Yra trys pagrindiniai būdai:
Entry points splitting – paprasčiausias, bet ne visada efektyviausias. Tiesiog apibrėžiate kelis entry point’us:
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js'
}
};
Dynamic imports – čia jau įdomiau. Naudojate import() funkciją, kad užkrautumėte kodą tik tada, kai reikia:
button.addEventListener('click', () => {
import('./heavyModule.js').then(module => {
module.doSomething();
});
});
SplitChunksPlugin – automatinis bendro kodo išskyrimas. Čia Webpack daro sunkų darbą už jus:
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
Aš paprastai kombinuoju visus tris metodus. Vendor bibliotekos eina į atskirą chunk’ą, kuris keičiasi retai – todėl naršyklė gali jį cache’inti. Sunkūs komponentai (kaip rich text editor’ius ar chart biblioteka) kraunami dinamiškai. O bendras utility kodas išskiriamas automatiškai.
Svarbu suprasti, kad daugiau chunk’ų ne visada reiškia geresnį performance’ą. Kiekvienas atskiras failas – tai papildomas HTTP request’as. Reikia rasti balansą tarp chunk’ų skaičiaus ir jų dydžio.
Tree shaking ir dead code elimination
Tree shaking – tai procesas, kai Webpack pašalina nebenaudojamą kodą. Skamba puikiai, bet realybėje veikia tik su ES6 moduliais ir ne visada taip efektyviai, kaip tikėtumėtės.
Kad tree shaking veiktų tinkamai, jūsų package.json turi turėti:
"sideEffects": false
Arba, jei turite failų su side effects (pvz., CSS importai):
"sideEffects": ["*.css", "*.scss", "./src/polyfills.js"]
Problema ta, kad daugelis npm paketų nėra tinkamai sukonfigūruoti tree shaking’ui. Pavyzdžiui, jei importuojate:
import { debounce } from 'lodash';
Vis tiek gaunate visą lodash biblioteką. Sprendimas:
import debounce from 'lodash/debounce';
Arba naudokite lodash-es, kuris jau yra ES modulių formatu.
Dar vienas dalykas – Babel. Jei jūsų Babel konfigūracija transformuoja ES6 modulius į CommonJS, tree shaking neveiks. Įsitikinkite, kad jūsų .babelrc arba babel.config.js turi:
{
"presets": [
["@babel/preset-env", {
"modules": false
}]
]
}
Loader’ių ir plugin’ų optimizavimas
Loader’iai gali būti didžiulis bottleneck’as build proceso metu. Babel-loader, ypač su TypeScript, gali užtrukti amžinybę dideliuose projektuose.
Pirma, naudokite include ir exclude opcijas, kad apribotumėte, kuriuos failus loader’is apdoroja:
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
include: path.resolve(__dirname, 'src'),
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
}
]
}
cacheDirectory: true yra būtinas. Tai leidžia Babel cache’inti transformacijos rezultatus. Antrą kartą build’inant, procesas bus daug greitesnis.
Jei naudojate TypeScript, apsvarstykite ts-loader alternatyvas. esbuild-loader yra neįtikėtinai greitas:
{
test: /\.tsx?$/,
loader: 'esbuild-loader',
options: {
loader: 'tsx',
target: 'es2015'
}
}
Viename projekte pakeitus ts-loader į esbuild-loader, build laikas sumažėjo nuo 45 sekundžių iki 8. Tai ne klaida – beveik 6 kartus greičiau.
Dėl CSS, jei naudojate CSS modulius ar SASS, įsitikinkite, kad production build’e naudojate MiniCssExtractPlugin vietoj style-loader:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
process.env.NODE_ENV === ‘production’
? MiniCssExtractPlugin.loader
: ‘style-loader’,
‘css-loader’,
‘sass-loader’
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: ‘[name].[contenthash].css’
})
]
};
Production build konfigūracija
Development ir production build’ai turėtų būti skirtingi. Development prioritetas – greitis ir debugging patogumas. Production – mažiausias bundle dydis ir geriausias runtime performance.
Štai kaip aš paprastai struktūrizuoju konfigūraciją:
// webpack.common.js
module.exports = {
entry: './src/index.js',
module: {
rules: [
// bendri loader'iai
]
}
};
// webpack.prod.js
const { merge } = require(‘webpack-merge’);
const common = require(‘./webpack.common.js’);
const TerserPlugin = require(‘terser-webpack-plugin’);
module.exports = merge(common, {
mode: ‘production’,
devtool: ‘source-map’,
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
}
},
parallel: true
})
],
moduleIds: ‘deterministic’,
runtimeChunk: ‘single’,
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: ‘vendors’,
chunks: ‘all’
}
}
}
}
});
moduleIds: ‘deterministic’ užtikrina, kad modulių ID būtų stabilūs tarp build’ų. Tai svarbu long-term caching’ui.
runtimeChunk: ‘single’ išskiria Webpack runtime kodą į atskirą failą. Tai mažas failas, bet jis keičiasi retai, todėl gali būti efektyviai cache’inamas.
drop_console: true pašalina visus console.log() iškvietimus production build’e. Tai ne tik sumažina bundle dydį, bet ir pagerina performance’ą.
Cache’inimo strategija ir versioning’as
Vienas iš didžiausių Webpack privalumų – galimybė generuoti failus su content hash’ais. Tai leidžia naudoti agresyvų browser caching’ą be baimės, kad vartotojai matys seną versiją po deploy’o.
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true
}
[contenthash] generuoja hash’ą pagal failo turinį. Jei failas nepasikeitė, hash’as lieka tas pats – browser’is naudos cache’intą versiją. Jei pasikeitė – naujas hash’as, browser’is parsisiųs naują versiją.
clean: true išvalo dist katalogą prieš kiekvieną build’ą. Seniau tam reikėjo atskiro plugin’o (clean-webpack-plugin), dabar tai built-in funkcionalumas.
Svarbu suprasti, kad net mažas pakeitimas viename faile gali pakeisti kelių failų hash’us. Pavyzdžiui, jei pakeičiate vendor chunk’ą, runtime chunk’as taip pat pasikeis, nes jame yra nuorodos į vendor chunk’ą.
Sprendimas – naudoti optimization.moduleIds: ‘deterministic’ ir optimization.runtimeChunk, kaip parodžiau anksčiau. Tai minimizuoja cascade efektą, kai vienas pakeitimas priverčia persigeneruoti visus failus.
Kas toliau: praktiniai patarimai iš tranšėjų
Po metų darbo su Webpack optimizacija įvairiuose projektuose, turiu keletą patarimų, kurie nėra akivaizdūs iš dokumentacijos.
Pirma, matuokite viską. Naudokite speed-measure-webpack-plugin, kad pamatytumėte, kiek laiko užtrunka kiekvienas loader’is ir plugin’as. Dažnai optimizuojate ne tą, kas iš tikrųjų lėtina build’ą.
Antra, neoptimizuokite per anksti. Jei jūsų bundle’as 500KB ir kraunasi per 2 sekundes – tai puiku. Nepraleiskite savaitės bandydami sumažinti iki 400KB. Yra svarbesnių dalykų, kuriuos galite daryti.
Trečia, atnaujinkite Webpack ir priklausomybes. Webpack 5 turi daug performance pagerinimų lyginant su 4 versija. Persistent caching, geresnis tree shaking, mažesni bundle’ai. Bet daug projektų vis dar sėdi ant senos versijos, nes „veikia ir taip”.
Ketvirta, naudokite Module Federation, jei turite micro-frontend architektūrą. Tai leidžia dalintis moduliais tarp skirtingų aplikacijų runtime metu, be poreikio bundle’inti viską kartu.
Penkta, eksperimentuokite su SWC. Tai Rust’u parašytas JavaScript/TypeScript kompiliatorius, kuris yra daug greitesnis už Babel. Webpack 5 turi swc-loader, kuris gali pakeisti babel-loader.
Šešta, optimizuokite images ir fonts. Dažnai visi susikoncentruoja į JavaScript, bet pamiršta, kad viena neoptimizuota nuotrauka gali svėrti daugiau už visą JS bundle’ą. Naudokite image-webpack-loader ar panašius sprendimus.
Septinta, stebėkite bundle dydį CI/CD pipeline’e. Yra įrankių kaip bundlesize ar size-limit, kurie gali automatiškai patikrinti, ar jūsų PR nepadidino bundle’o per daug. Tai apsaugo nuo laipsniško bundle’o augimo.
Optimizavimas nėra vienkartinis procesas. Tai nuolatinė kova su entropija. Kiekviena nauja priklausomybė, kiekvienas naujas feature – tai potencialus bundle’o dydžio padidėjimas. Bet su tinkamais įrankiais ir procesais, galite išlaikyti jūsų aplikaciją greitą ir efektyvią, nepaisant jos augimo.
Webpack gali atrodyti sudėtingas, bet iš tikrųjų tai tik įrankis. Kaip ir bet kuris įrankis, jis reikalauja laiko išmokti tinkamai naudoti. Tačiau kai suprantate, kaip jis veikia, galite pasiekti įspūdingų rezultatų. Mažesni bundle’ai, greitesnis loading’as, laimingesni vartotojai – kas gali būti geriau?
