Kas tas tree shaking ir kodėl turėtų rūpėti
Kai kuriate modernią web aplikaciją, greičiausiai naudojate kokį nors bundlerį – Webpack, Rollup, Vite ar panašų įrankį. Ir turbūt pastebėjote, kad galutinis JavaScript failas kartais būna nemažas. Čia ir ateina į pagalbą tree shaking – procesas, kuris automatiškai pašalina kodą, kurio niekas nenaudoja.
Pavadinimas atėjo iš tikrai paprastos metaforos: įsivaizduokite medį, kurį purtysite. Sveiki, stiprūs vaisiai (naudojamas kodas) lieka ant šakų, o nudžiūvę lapai ir šiukšlės (nenaudojamas kodas) nukrenta žemėn. Tik mūsų atveju bundleris „purto” jūsų kodo medį ir palieka tik tai, kas tikrai reikalinga.
Realybėje tai reiškia, kad jei importuojate biblioteką, kuri turi 50 funkcijų, bet naudojate tik 3, galutiniame bundle turėtų atsirasti tik tos 3 funkcijos. Teoriškai. Praktikoje viskas šiek tiek sudėtingiau, bet apie tai kalbėsime vėliau.
Kaip veikia tree shaking po gaubtu
Tree shaking remiasi ES6 modulių sistema ir jos statine struktūra. Tai reiškia, kad import ir export sakiniai turi būti viršutiniame lygyje ir negali būti sąlyginiai. Bundleris gali išanalizuoti visus importus ir eksportus dar prieš vykdant kodą.
Štai paprastas pavyzdys. Turite failą utils.js:
export function naudojama() {
return 'Ši funkcija naudojama';
}
export function nenaudojama() {
return 'Niekas manęs nekvies';
}
export function taipatNenaudojama() {
console.log('Aš taip pat nereikalinga');
}
O jūsų pagrindiniame faile:
import { naudojama } from './utils.js';
console.log(naudojama());
Bundleris su įjungtu tree shaking galutiniame bundle paliks tik naudojama funkciją. Kitos dvi tiesiog nepateks į galutinį failą. Taip sutaupote baitų, o svarbiausia – naršyklė neturi parsinti ir kompiliuoti kodo, kurio vis tiek niekas nenaudos.
Bet čia svarbu suprasti vieną dalyką: tree shaking veikia tik su ES moduliais. Jei naudojate require() ir module.exports (CommonJS), tree shaking neveiks. Todėl modernios bibliotekos dažniausiai pristato du variantus – CommonJS ir ES modulių.
Kodėl kartais tree shaking neveikia taip, kaip tikitės
Štai čia ir prasideda įdomiausia dalis. Teoriškai tree shaking skamba puikiai, bet praktikoje susidursite su situacijomis, kai jis tiesiog neveikia arba veikia ne taip efektyviai, kaip norėtųsi.
Pirma problema – šalutiniai efektai (side effects). Jei bundleris negali būti tikras, ar kodas neturi šalutinių efektų, jis to kodo nepašalins. Pavyzdžiui:
export function skaiciuoti() {
window.rezultatas = 42; // Šalutinis efektas!
return 42;
}
Net jei niekas neimportuoja šios funkcijos, bundleris gali jos nepašalinti, nes ji modifikuoja globalų objektą. Arba dar blogesnis variantas:
import './styles.css'; // Ar tai turi šalutinių efektų? import 'some-polyfill'; // O čia?
Bundleris paprastai nelabai nori rizikuoti ir palieka tokius importus ramybėje. Todėl moderniose bibliotekose package.json faile rasite lauką "sideEffects", kuris nurodo, kurie failai tikrai neturi šalutinių efektų:
{
"sideEffects": false
}
Arba konkrečiau:
{
"sideEffects": ["*.css", "src/polyfills.js"]
}
Antra problema – kaip rašote kodą. Tree shaking veikia su named exports, bet ne su default exports. Na, veikia, bet ne taip efektyviai. Pavyzdžiui:
// Gerai tree shaking
export const a = 1;
export const b = 2;
// Blogai tree shaking
export default {
a: 1,
b: 2
};
Antruoju atveju bundleris mato tik vieną objektą ir negali jo „išardyti” į atskiras dalis.
Realūs pavyzdžiai su populiariomis bibliotekomis
Paimkime Lodash – vieną populiariausių JavaScript utility bibliotekų. Jei rašysite:
import _ from 'lodash'; _.debounce(myFunction, 300);
Į jūsų bundle pateks visa Lodash biblioteka – apie 70KB (minimizuota). Bet jei naudosite ES modulių versiją:
import debounce from 'lodash-es/debounce'; debounce(myFunction, 300);
Gausit tik debounce funkciją ir jos priklausomybes – gal 2-3KB. Skirtumas akivaizdus.
Arba paimkime Material-UI (dabar MUI). Jei importuojate taip:
import { Button, TextField } from '@mui/material';
Teoriškai turėtų veikti tree shaking, bet praktikoje dažnai vis tiek importuojasi per daug. Geriau naudoti tiesiogines importus:
import Button from '@mui/material/Button'; import TextField from '@mui/material/TextField';
Arba dar geriau – naudoti babel pluginą, kuris automatiškai transformuoja importus:
// Rašote
import { Button } from '@mui/material';
// Babel transformuoja į
import Button from '@mui/material/Button';
Webpack konfigūracija tree shaking optimizavimui
Jei naudojate Webpack, tree shaking production mode įjungiamas automatiškai. Bet yra keletas niuansų, kuriuos verta žinoti.
Pirma, įsitikinkite, kad nenaudojate Babel su modules: 'commonjs' nustatymu. Jūsų .babelrc turėtų atrodyti maždaug taip:
{
"presets": [
["@babel/preset-env", {
"modules": false
}]
]
}
Tas "modules": false nurodo Babel nepakeisti ES modulių į CommonJS, nes kitaip tree shaking neveiks.
Antra, naudokite usedExports optimizaciją:
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true,
sideEffects: true
}
};
Čia usedExports analizuoja, kas tikrai naudojama, minimize suspaudžia kodą, o sideEffects atsižvelgia į package.json nustatymus.
Trečia, galite naudoti webpack-bundle-analyzer, kad pamatytumėte, kas tiksliai pateko į jūsų bundle:
npm install --save-dev webpack-bundle-analyzer
Ir webpack config:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
Paleidus build, atsidaro interaktyvus žemėlapis, kuriame matote visus modulius ir jų dydžius. Labai patogu identifikuoti, kur slypi problemos.
Rollup ir Vite – tree shaking meistrų įrankiai
Rollup nuo pat pradžių buvo sukurtas su tree shaking mintyje. Jis daro tai geriau nei Webpack, nes jo pagrindinė paskirtis – kurti bibliotekas, o ne aplikacijas. Jei kuriate npm paketą, Rollup greičiausiai bus geresnis pasirinkimas.
Rollup konfigūracija tree shaking yra labai paprasta:
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'es'
},
treeshake: {
moduleSideEffects: false
}
};
Vite, kuris po gaubtu naudoja Rollup production build’ams, taip pat puikiai tvarko tree shaking. Jums net nereikia nieko konfigūruoti – viskas veikia iš dėžės:
// vite.config.js
export default {
build: {
rollupOptions: {
// Čia galite pridėti papildomų Rollup nustatymų
}
}
}
Vienas įdomus Rollup privalumas – jis gali pašalinti net nenaudojamus class metodus. Webpack to nedaro, nes bijo, kad metodai gali būti naudojami per reflekciją ar kitais dinamiškais būdais.
Praktiniai patarimai kasdieniam darbui
Dabar keletas konkrečių rekomendacijų, kurias galite pritaikyti jau šiandien.
Visuomet naudokite named exports vietoj default:
// Geriau
export function myFunction() {}
export const myConstant = 42;
// Blogiau
export default {
myFunction: function() {},
myConstant: 42
};
Importuokite tik tai, ko reikia:
// Geriau
import { specific } from 'library';
// Blogiau
import * as everything from 'library';
Patikrinkite bibliotekų dokumentaciją. Daugelis modernių bibliotekų turi specialius patarimus, kaip importuoti, kad veiktų tree shaking. Pavyzdžiui, date-fns rekomenduoja:
import format from 'date-fns/format'; import parseISO from 'date-fns/parseISO';
Vietoj:
import { format, parseISO } from 'date-fns';
Venkite dinaminių importų, kai reikia tree shaking. Dinaminis import() yra puikus code splitting, bet jis neleidžia bundleriui atlikti statinės analizės:
// Tree shaking neveiks const moduleName = condition ? 'moduleA' : 'moduleB'; import(moduleName);
Naudokite TypeScript su "module": "esnext". TypeScript kompiliatorius taip pat gali „sugadinti” tree shaking, jei neteisingai sukonfigūruotas:
{
"compilerOptions": {
"module": "esnext",
"target": "esnext"
}
}
Kai tree shaking susiduria su realybe
Pabaigai norisi pasakyti, kad tree shaking nėra sidabrinė kulka. Tai puikus įrankis, bet jis turi savo apribojimus. Kartais pastebėsite, kad net ir viską teisingai sukonfigūravus, bundle vis tiek didesnis, nei tikėjotės.
Dažniausiai problema slypi ne jūsų kode, o trečiųjų šalių bibliotekose. Ne visos bibliotekos parašytos su tree shaking mintyse. Kai kurios vis dar naudoja CommonJS, kitos turi daug šalutinių efektų, trečios tiesiog blogai suprojektuotos.
Todėl prieš įtraukdami naują biblioteką į projektą, verta patikrinti keletą dalykų. Ar ji turi ES modulių versiją? Ar package.json yra "module" laukas? Ar nurodyta "sideEffects"? Galite naudoti įrankius kaip bundlephobia.com, kuris parodo, kiek biblioteka pridės prie jūsų bundle.
Ir nepamirškite, kad tree shaking – tik viena optimizavimo dalis. Code splitting, lazy loading, caching strategijos – visa tai kartu sudaro efektyvią aplikaciją. Tree shaking padeda sumažinti pradinį bundle dydį, bet jei vartotojas vis tiek turi parsisiųsti 500KB JavaScript, gal verta pagalvoti apie architektūrą plačiau.
Galiausiai, naudokite analitinius įrankius. Webpack Bundle Analyzer, source-map-explorer, ar net Chrome DevTools Coverage tab – visa tai padės suprasti, kur tiksliai dingsta baitai ir ar tree shaking tikrai veikia taip, kaip tikėjotės. Kartais rezultatai gali nustebinti – galbūt ta „lengva” biblioteka iš tikrųjų įtraukia pusę interneto, arba atvirkščiai, ta „sunki” biblioteka puikiai optimizuota ir prideda tik tai, ko reikia.
