Articles

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 importsquare 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 som components/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 och export).
  • 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 projektets package.json fil.
  • användproductionmode 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.