Trädskakning
trädskakning är en term som vanligtvis används i JavaScript-sammanhanget för eliminering av död kod. Det bygger på den statiska strukturen i ES2015 modulsyntax, dvs import
och export
. Namnet och konceptet har populariserats av ES2015-modulen Bundler rollup.
Webpack 2-utgåvan kom med inbyggt stöd för ES2015-moduler (alias harmony-moduler) samt oanvänd modulexportdetektering. Den nya webpack 4-utgåvan expanderar på den här funktionen med ett sätt att ge tips till kompilatorn via "sideEffects"
package.json
egenskapen för att ange vilka filer i ditt projekt som är ”rena” och därför säkra att beskära om de inte används.
Lägg till ett verktyg
Låt oss lägga till en ny verktygsfil till vårt projekt, src/math.js
, som exporterar två funktioner:
projekt
webpack-demo|- package.json|- webpack.config.js|- /dist |- bundle.js |- index.html|- /src |- index.js+ |- math.js|- /node_modules
src/math.JS
export function square(x) { return x * x;}export function cube(x) { return x * x * x;}
Ställ inmode
konfigurationsalternativ till utveckling för att se till att buntet inte minifieras:
webpack.konfiguration.js
const path = require('path');module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), },+ mode: 'development',+ optimization: {+ usedExports: true,+ },};
med det på plats, låt oss uppdatera vårt inmatningsskript för att använda en av dessa nya metoder och ta bort lodash
för enkelhet:
src/index.js
- import _ from 'lodash';+ import { cube } from './math.js'; function component() {- const element = document.createElement('div');+ const element = document.createElement('pre');- // Lodash, now imported by this script- element.innerHTML = _.join(, ' ');+ element.innerHTML = .join('\n\n'); return element; } document.body.appendChild(component());
Observera att vi inte import
square
metoden från src/math.js
modulen. Den funktionen är vad som kallas ”död kod”, vilket betyder ett oanvänt export
som ska släppas. Låt oss nu köra vårt npm-skript, npm run build
och inspektera utmatningspaketet:
dist/bundle.js (runt raderna 90-100)
/* 1 *//***/ (function (module, __webpack_exports__, __webpack_require__) { 'use strict'; /* unused harmony export square */ /* harmony export (immutable) */ __webpack_exports__ = cube; function square(x) { return x * x; } function cube(x) { return x * x * x; }});
notera unused harmony export square
kommentar ovan. Om du tittar på koden under den märker du att square
inte importeras, men det ingår fortfarande i paketet. Vi fixar det i nästa avsnitt.
markera filen som biverkningsfri
i en 100% ESM-modulvärld är det enkelt att identifiera biverkningar. Men vi är inte där ännu, så under tiden är det nödvändigt att ge tips till webpacks kompilator på ”renhet” av din kod.
Så här uppnås är"sideEffects"
paketet.JSON egendom.
{ "name": "your-project", "sideEffects": false}
all kod som anges ovan innehåller inte biverkningar, så vi kan helt enkelt markera egenskapen som false
för att informera webpack om att den säkert kan beskära oanvänd export.
om din kod hade några biverkningar kan dock en array tillhandahållas istället:
{ "name": "your-project", "sideEffects": }
arrayen accepterar enkla globmönster till relevanta filer. Den använder glob-to-regexp under huven (stöder: *
**
{a,b}
). Mönster som
*.css
, som inte innehåller ett /
, kommer att behandlas som **/*.css
.
{ "name": "your-project", "sideEffects": }
slutligen kan "sideEffects"
också ställas in från module.rules
konfigurationsalternativ.
klargörande trädskakning och sidoeffekter
sideEffects
ochusedExports
(mer känd som trädskakning) optimeringar är två olika saker.
sideEffects
är mycket effektivare eftersom det gör det möjligt att hoppa över hela moduler/filer och hela underträdet.
usedExports
förlitar sig på terser för att upptäcka biverkningar i uttalanden. Det är en svår uppgift i JavaScript och inte lika effektiv som enkel sideEffects
flagga. Det kan inte heller hoppa över subtree / beroenden eftersom specifikationen säger att biverkningar måste utvärderas. Medan exportfunktionen fungerar bra är reacts högre Orderkomponenter (HOC) problematiska i detta avseende.
Låt oss göra ett exempel:
import { Button } from '@shopify/polaris';
den förbundna versionen ser ut så här:
import hoistStatics from 'hoist-non-react-statics';function Button(_ref) { // ...}function merge() { var _final = {}; for ( var _len = arguments.length, objs = new Array(_len), _key = 0; _key < _len; _key++ ) { objs = arguments; } for (var _i = 0, _objs = objs; _i < _objs.length; _i++) { var obj = _objs; mergeRecursively(_final, obj); } return _final;}function withAppProvider() { return function addProvider(WrappedComponent) { var WithProvider = /*#__PURE__*/ (function (_React$Component) { // ... return WithProvider; })(Component); WithProvider.contextTypes = WrappedComponent.contextTypes ? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes) : polarisAppProviderContextTypes; var FinalComponent = hoistStatics(WithProvider, WrappedComponent); return FinalComponent; };}var Button$1 = withAppProvider()(Button);export { // ..., Button$1,};
När Button
är oanvänd kan du effektivt ta bort export { Button$1 };
som lämnar all återstående kod. Så frågan Är ”har den här koden några biverkningar eller kan den tas bort på ett säkert sätt?”. Svårt att säga, särskilt på grund av denna rad withAppProvider()(Button)
withAppProvider
kallas och returvärdet kallas också. Finns det några biverkningar när du ringer merge
eller hoistStatics
? Finns det biverkningar när du tilldelar WithProvider.contextTypes
(Setter?) eller när du läser WrappedComponent.contextTypes
(Getter?).
Terser försöker faktiskt räkna ut det, men det vet inte säkert i många fall. Det betyder inte att terser inte gör sitt jobb bra eftersom det inte kan räkna ut det. Det är bara för svårt att bestämma det på ett tillförlitligt sätt på ett dynamiskt språk som JavaScript.
men vi kan hjälpa terser genom att använda/*#__PURE__*/
annotation. Det flaggar ett uttalande som bieffekt fri. Så en enkel förändring skulle göra det möjligt att trädskaka koden:
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
detta skulle göra det möjligt att ta bort den här koden. Men det finns fortfarande frågor med importen som måste inkluderas/utvärderas eftersom de kan innehålla biverkningar.
för att hantera detta använder vi egenskapen"sideEffects"
Ipackage.json
.
det liknar /*#__PURE__*/
men på en modulnivå istället för en uttalande nivå. Det står ("sideEffects"
property): ”om ingen direkt export från en modul flaggad utan sidoeffekter används, kan buntaren hoppa över utvärderingen av modulen för biverkningar.”.
i Shopifys Polaris-exempel ser originalmoduler ut så här:
index.js
import './configure';export * from './types';export * from './components';
komponenter/index.JS
// ...export { default as Breadcrumbs } from './Breadcrumbs';export { default as Button, buttonFrom, buttonsFrom } from './Button';export { default as ButtonGroup } from './ButtonGroup';// ...
paket.json
// ..."sideEffects": ,// ...
för import { Button } from "@shopify/polaris";
detta har följande konsekvenser:
- inkludera det: inkludera modulen, utvärdera den och fortsätt analysera beroenden
- hoppa över: inkludera inte det, utvärdera det inte men fortsätt analysera beroenden
- Uteslut det: inkludera inte det, utvärdera inte det och analysera inte beroenden
specifikt per matchande resurs(er):
-
index.js
: Ingen direkt export används, men flaggas med sidoeffekter -> inkludera det -
configure.js
: ingen export används, men flaggas med sidoeffekter -> inkludera det -
types/index.js
: ingen export används, inte flaggad med sidoeffekter -> Uteslut det -
components/index.js
: ingen direkt export används, inte flaggad med sidoeffekter, men reexporterad export används -> hoppa över -
components/Breadcrumbs.js
: Ingen export används, inte flaggad med sidoeffekter – > Uteslut det. Detta utesluter också alla beroenden somcomponents/Breadcrumbs.css
även om de flaggas med sidoeffekter. -
components/Button.js
: direkt export används, inte flaggad med sidoeffekter -> inkludera det -
components/Button.css
: ingen export används, men flaggad med sidoeffekter -> inkludera det
i detta fall ingår endast 4 moduler i buntet:
-
index.js
: ganska mycket Tom configure.js
components/Button.js
components/Button.css
Efter denna optimering kan andra optimeringar fortfarande gälla. Till exempel: buttonFrom
och buttonsFrom
export från Button.js
är oanvända också. usedExports
optimering kommer att hämta det och terser kanske kan släppa några uttalanden från modulen.
Modulsammanfogning gäller också. Så att dessa 4 moduler plus ingångsmodulen (och förmodligen fler beroenden) kan sammanfogas. index.js
har ingen kod genererad i slutet.
markera ett funktionsanrop som bieffektfritt
det är möjligt att berätta för webpack att ett funktionsanrop är bieffektfritt (rent) genom att använda /*#__PURE__*/
annotering. Det kan sättas framför funktionsanrop för att markera dem som bieffektfria. Argument som skickas till funktionen markeras inte av anteckningen och kan behöva markeras individuellt. När initialvärdet i en variabeldeklaration för en oanvänd variabel betraktas som bieffektfri (ren), blir den markerad som död kod, inte exekverad och tappad av minimisatorn. Detta beteende är aktiverat när optimization.innerGraph
är inställt på true
.
fil.js
/*#__PURE__*/ double(55);
Minifiera utmatningen
Så vi har cued upp vår” döda kod”för att släppas med hjälp av import
och export
syntax, men vi måste fortfarande släppa den från bunten. För att göra det, Ställ in mode
konfigurationsalternativ till production
.
webbpaket.konfiguration.js
const path = require('path');module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), },- mode: 'development',- optimization: {- usedExports: true,- }+ mode: 'production',};
med det kvadrerade bort kan vi köra en annan npm run build
och se om något har ändrats.
Lägg märke till något annat om dist/bundle.js
? Helt klart är hela buntet nu minifierat och manglat, men om du tittar noga ser du inte funktionen square
men kommer att se en manglad version av cube
funktion (function r(e){return e*e*e}n.a=r
). Med minifiering och trädskakning är vårt bunt nu några byte mindre! Även om det kanske inte verkar så mycket i detta konstruerade exempel kan trädskakning ge en signifikant minskning av buntstorleken när man arbetar med större applikationer med komplexa beroendeträd.
slutsats
Så, vad vi har lärt oss är att för att dra nytta av trädskakning måste du…
- använd ES2015 modulsyntax (dvs.
import
ochexport
). - se till att inga kompilatorer omvandlar din ES2015-modulsyntax till CommonJS-moduler (detta är standardbeteendet för den populära Babel preset @babel/preset – env-se dokumentationen för mer information).
- Lägg till en
"sideEffects"
egenskap till projektetspackage.json
fil. - använd
production
mode
konfigurationsalternativ för att aktivera olika optimeringar inklusive minifiering och trädskakning.
Du kan föreställa dig din ansökan som ett träd. Källkoden och biblioteken du faktiskt använder representerar de gröna, levande bladen på trädet. Död kod representerar de bruna, döda bladen på trädet som konsumeras av hösten. För att bli av med de döda bladen måste du skaka trädet och få dem att falla.
om du är intresserad av fler sätt att optimera din produktion, Hoppa till nästa guide för detaljer om byggnad för produktion.