Articles

Tree Shaking

Tree shaking to termin powszechnie używany w kontekście JavaScript do eliminacji martwego kodu. Opiera się na statycznej strukturze składni modułu ES2015, tj. import I export. Nazwa i koncepcja zostały spopularyzowane przez moduł ES2015 bundler rollup.

wydanie webpack 2 zawiera wbudowaną obsługę modułów ES2015 (alias harmony modules), a także wykrywanie eksportu nieużywanych modułów. Nowe wydanie webpack 4 rozszerza tę możliwość o sposób dostarczania wskazówek do kompilatora za pomocą właściwości"sideEffects"package.json, aby określić, które pliki w projekcie są „czyste” i dlatego bezpieczne do przycinania, jeśli nie są używane.

Dodaj narzędzie

dodajmy nowy plik narzędzia do naszego projektu,src/math.js, który eksportuje dwie funkcje:

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;}

ustaw opcję konfiguracjimode, aby upewnić się, że pakiet nie jest minifikowany:

webpack.config.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,+ },};

mając to na miejscu, zaktualizujmy nasz skrypt wejściowy, aby użyć jednej z tych nowych metod i usuńlodash dla uproszczenia:

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());

zauważ, że nie zrobiliśmy import metody square z modułu src/math.js. Ta funkcja jest znana jako” martwy Kod”, co oznacza nieużywany export, który powinien zostać usunięty. Teraz uruchom nasz skrypt npm ,npm run build I sprawdź pakiet wyjściowy:

dist/bundle.js (około linii 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; }});

zwróć uwagę na unused harmony export square komentarz powyżej. Jeśli spojrzysz na poniższy kod, zauważysz, że square nie jest importowany, jednak nadal jest zawarty w pakiecie. Naprawimy to w następnej sekcji.

Oznacz plik jako wolny od efektów ubocznych

w świecie modułów 100% ESM, identyfikacja skutków ubocznych jest prosta. Jednak nie jesteśmy tam jeszcze, więc w międzyczasie konieczne jest dostarczenie kompilatorowi webpack wskazówek na temat „czystości” kodu.

sposób, w jaki jest to realizowane, to"sideEffects" pakiet.własność json.

{ "name": "your-project", "sideEffects": false}

cały powyższy kod nie zawiera efektów ubocznych, więc możemy po prostu oznaczyć właściwość jakofalse, aby poinformować webpack, że może bezpiecznie przyciąć niewykorzystany eksport.

Jeśli Twój kod miał pewne skutki uboczne, zamiast tego można podać tablicę:

{ "name": "your-project", "sideEffects": }

tablica akceptuje proste wzorce glob do odpowiednich plików. Używa glob-to-regexp pod maską (obsługuje: ***{a,b}). Wzorce takie jak *.css, które nie zawierają /, będą traktowane jak **/*.css.

{ "name": "your-project", "sideEffects": }

wreszcie"sideEffects" można również ustawić z opcji konfiguracjimodule.rules.

Clarifying Tree shaking and sideEffects

optymalizacjesideEffects IusedExports (bardziej znane jako Tree shaking) to dwie różne rzeczy.

sideEffects jest znacznie bardziej efektywny, ponieważ pozwala na pominięcie całych modułów / plików i całego podzbioru.

usedExports polega na wykrywaniu efektów ubocznych w oświadczeniach. Jest to trudne zadanie w JavaScript i nie tak skuteczne jak prostasideEffects flaga. Nie można również pominąć podzbioru / zależności, ponieważ specyfikacja mówi, że efekty uboczne muszą zostać ocenione. Podczas gdy funkcja eksportu działa poprawnie, komponenty wyższego rzędu (HOC) Reacta są w tym względzie problematyczne.

zróbmy przykład:

import { Button } from '@shopify/polaris';

wstępnie dołączona wersja wygląda następująco:

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,};

gdy Button jest nieużywana, możesz skutecznie usunąć export { Button$1 }; który pozostawia cały pozostały kod. Więc pytanie brzmi: „czy ten kod ma jakieś skutki uboczne lub można go bezpiecznie usunąć?”. Trudno powiedzieć, zwłaszcza z powodu tej linii withAppProvider()(Button)withAppProvider i zostanie również wywołana wartość zwracana. Czy są jakieś skutki uboczne podczas wywoływania merge lub hoistStatics? Czy są skutki uboczne przy przypisywaniu WithProvider.contextTypes (Setter?) lub podczas czytania WrappedComponent.contextTypes (Getter?).

Terser faktycznie próbuje to rozgryźć, ale w wielu przypadkach nie wie na pewno. Nie oznacza to, że terser nie wykonuje swojej pracy dobrze, ponieważ nie może tego rozgryźć. Jest to po prostu zbyt trudne do wiarygodnego określenia w dynamicznym języku, takim jak JavaScript.

ale możemy pomóc terserowi, używając adnotacji /*#__PURE__*/. Oznacza oświadczenie jako efekt uboczny wolny. Tak więc prosta zmiana umożliwiłaby drzewiaste potrząsanie kodem:

var Button$1 = /*#__PURE__*/ withAppProvider()(Button);

pozwoliłoby to usunąć ten fragment kodu. Nadal jednak istnieją pytania dotyczące przywozu, które należy uwzględnić/ocenić, ponieważ mogą one zawierać skutki uboczne.

aby temu zaradzić, używamy właściwości"sideEffects" wpackage.json.

jest podobny do/*#__PURE__*/ ale na poziomie modułu zamiast poziomu instrukcji. To mówi ("sideEffects" właściwość): „jeśli nie jest używany bezpośredni eksport z modułu oznaczonego bez efektów ubocznych, bundler może pominąć ocenę modułu pod kątem efektów ubocznych.”.

w przykładzie Shopify Polaris oryginalne Moduły wyglądają następująco:

index.js

import './configure';export * from './types';export * from './components';

komponenty/indeks.js

// ...export { default as Breadcrumbs } from './Breadcrumbs';export { default as Button, buttonFrom, buttonsFrom } from './Button';export { default as ButtonGroup } from './ButtonGroup';// ...

pakiet.json

// ..."sideEffects": ,// ...

Dla import { Button } from "@shopify/polaris"; ma to następujące implikacje:

  • włącz: Dołącz moduł, oceń go i kontynuuj analizę zależności
  • pomiń: nie włączaj, nie oceniaj, ale kontynuuj analizę zależności
  • wyłącz go: nie włączaj, nie oceniaj oceniaj je i nie analizuj zależności

specjalnie dla pasujących zasobów:

  • index.js: Nie jest używany bezpośredni eksport, ale oznaczony sideEffects -> włącz go
  • configure.js: nie jest używany eksport, ale oznaczony sideEffects -> włącz go
  • types/index.js: nie jest używany eksport, nie jest oznaczony efektami side -> wyklucz go
  • components/index.js: nie jest używany Eksport bezpośredni, nie jest oznaczony efektami side, ale używany jest eksport ponownie eksportowany -> przejdź do
  • components/Breadcrumbs.js: Nie jest używany eksport, nie jest oznaczany za pomocą sideEffects- > wyklucz go. To również wykluczyło wszystkie zależności ,takie jakcomponents/Breadcrumbs.css, nawet jeśli są oznaczone sideEffects.
  • components/Button.js: używany jest bezpośredni eksport, nie oznaczony sideEffects -> dołącz go
  • components/Button.css: nie jest używany eksport, ale oznaczony sideEffects -> dołącz go

w tym przypadku pakiet zawiera tylko 4 moduły:

  • index.js: prawie pusty
  • configure.js
  • components/Button.js
  • components/Button.css

Po tej optymalizacji mogą nadal obowiązywać inne optymalizacje. Na przykład: buttonFrom I buttonsFrom eksport z Button.js również nie są używane. usedExports optymalizacja go podniesie i terser może być w stanie usunąć niektóre instrukcje z modułu.

obowiązuje również konkatenacja modułu. Tak, że te 4 moduły plus moduł wejściowy (i prawdopodobnie więcej zależności) mogą być połączone. index.js na końcu nie wygenerowano kodu.

Oznacz wywołanie funkcji jako wolne od efektów ubocznych

można powiedzieć webpack, że wywołanie funkcji jest wolne od efektów ubocznych (czyste) za pomocą adnotacji/*#__PURE__*/. Można go umieścić przed wywołaniami funkcji, aby oznaczyć je jako wolne od efektów ubocznych. Argumenty przekazywane do funkcji nie są oznaczane przez adnotację i mogą wymagać oznaczania osobno. Gdy wartość początkowa w deklaracji zmiennej nieużywanej zmiennej jest uważana za wolną od efektów ubocznych (czystą), jest ona oznaczana jako martwy kod, nie jest wykonywana i upuszczana przez minimalizator. To zachowanie jest włączone, gdyoptimization.innerGraph jest ustawione natrue.

plik.js

/*#__PURE__*/ double(55);

minimalizuje wyjście

, więc wskazaliśmy nasz „martwy kod”, aby został usunięty za pomocą składniimportIexport, ale nadal musimy go usunąć z pakietu. Aby to zrobić, ustawmode opcję konfiguracji naproduction.

webpack.config.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',};

z tym do kwadratu, możemy uruchomić innynpm run build I sprawdzić, czy coś się zmieniło.

zauważyłeś coś innego na temat dist/bundle.js? Oczywiście cały pakiet jest teraz minifikowany i zniekształcony, ale jeśli przyjrzysz się uważnie, nie zobaczysz funkcji square, ale zobaczysz zniekształconą wersję funkcji cubefunction r(e){return e*e*e}n.a=r). Dzięki minifikacji i tree shaking nasz pakiet jest teraz o kilka bajtów mniejszy! Chociaż w tym wymyślonym przykładzie może się to nie wydawać zbyt wiele, drżenie drzewa może spowodować znaczny spadek rozmiaru pakietu podczas pracy nad większymi APLIKACJAMI ze złożonymi drzewami zależności.

wniosek

więc nauczyliśmy się, że aby skorzystać z potrząsania drzew, musisz…

  • użyj składni modułu ES2015 (tj. importI export).
  • upewnij się, że Kompilatory nie przekształcają składni modułu ES2015 w Moduły CommonJS (jest to domyślne zachowanie popularnego Babel preset @babel/preset-env – więcej szczegółów w dokumentacji).
  • Dodaj"sideEffects" właściwość do plikupackage.json.
  • użyj opcji konfiguracjiproductionmode, aby włączyć różne optymalizacje, w tym minifikację i potrząsanie drzewem.

możesz sobie wyobrazić swoją aplikację jako drzewo. Kod źródłowy i biblioteki, których faktycznie używasz, reprezentują zielone, żywe liście drzewa. Martwy kod reprezentuje brązowe, martwe liście drzewa, które są spożywane przez Jesień. Aby pozbyć się martwych liści, musisz potrząsnąć drzewem, powodując ich upadek.

Jeśli jesteś zainteresowany innymi sposobami optymalizacji produkcji, przejdź do następnego przewodnika, aby uzyskać szczegółowe informacje na temat budynku do produkcji.