Tree-shaking. Jak wyeliminować nieużywany kod z naszego bundle.js?
Tree-shaking to jedna z funkcji, z której korzystają bundlery takie jak webpack, rollup czy parcel, która pozwala nam zmniejszyć wagę kodu wynikowego naszej aplikacji, a co za tym idzie zwiększyć tzw. performance aplikacji. Dzieje się to poprzez przefiltrowanie zaimportowanych przez nas zależności i wykluczenie z kodu wynikowego kodu tych, które są zbędne.
Spis treści
Module bundlers
Żeby wytłumaczyć jak działa tree-shaking, przypomnijmy sobie w ogóle czym są wspomniane bundlery. W kontekście aplikacji JavaScriptowych są to narzędzia, które biorą plik lub pliki JavaScriptowe wraz z ich zależnościami i łączą je w najczęściej jeden lub kilka plików wynikowych, często stosując przy tym różne optymalizacje, np:
- konkatenacja – łączenie wielu plików w jeden, żeby ograniczyć ilość requestów,
- minifikacja – pozbycie się zbędnych znaków z kodu, aby zmniejszyć jego wagę,
- skracanie nazw zmiennych,
- code-splitting – dzielenie kodu na kilka plików wynikowych, które są wczytywane na żądanie tylko w tych miejscach aplikacji, w których są potrzebne,
- tree-shaking – pozbywanie się nieużywanych części kodu – przydatne np. kiedy korzystamy tylko z kilku funkcji biblioteki jQuery, a nie chcemy włączać całej, sporo ważącej biblioteki do naszego kodu wynikowego.
Dlaczego tree-shaking?
Nazwa tree-shaking jest bardzo trafna. Wyobraźcie sobie, że drzewo zależności naszej aplikacji to takie niewielkich rozmiarów prawdziwe drzewko, które jesteśmy w stanie wziąć do ręki – wszystkie zależności, z których korzystamy to zielone, świetnie się mające rozwinięte gałęzie i liście, a te zależności, z których nie korzystamy w aplikacji, a które zaimportowaliśmy w kodzie źródłowym to te wyschnięte, ledwo się trzymające. Jeśli takim drzewem potrząchniemy to te ledwo trzymające się gałęzie – czyli nasz nieużywany kod, po prostu odpadnie.
Bez tree-shakingu
Załóżmy, że nie korzystamy z bundlera, który oferuje tree-shaking, budujemy aplikacje i mamy taki plik z matematycznymi funkcjami pomocniczymi, które stworzyliśmy sobie już kiedyś, podesłał nam znajomy lub pobraliśmy go z menadżera pakietów, np. npm. Teraz załóżmy, że w naszej aplikacji chcemy skorzystać z funkcji add.
Importujemy funkcję za pomocą słowa kluczowego import, add(1, 2) powinno nam zwrócić 3, a więc wszystko działa jak należy.
Jest tu jednak pewien haczyk.
Mimo że użyliśmy destrukturyzacji i na pierwszy rzut oka wygląda jakbyśmy importowali tylko funkcję add to tak naprawdę do pakietu wynikowego dołączany jest cały plik mathUtils, a więc również funkcje minus, multiple i divide, z których w naszej aplikacji zupełnie nie korzystamy co niepotrzebnie zwiększa wagę naszej aplikacji.
Z tree-shakingiem
Teraz wyobraźmy sobie scenariusz, w którym nasz bundler korzysta z tree-shakingu.
Możemy zrobić nawet coś takiego jak poniżej, czyli zaimportować funkcję divide, a obecnie najnowsza wersja webpacka pozbędzie się z kodu wynikowego nie tylko funkcji minus, multiple, ale również divide, ze względu na to, że nie jest ona nigdzie używana. Super.
Kiedy tree-shaking nie działa?
Tree-shaking polega na statycznym działaniu modułów ES6, a więc nie zadziała albo to działanie będzie utrudnione i mniej efektywne na zaimportowanych plikach, które korzystają z modułów CommonJS, które są dynamiczne, tzn. możemy je zaimportować w trakcie działania aplikacji pod jakimś konkretnym warunkiem. A to sprawia, że webpack czy inny bundler nie jest w stanie zadecydować, które z modułów zostaną użyte zanim nie włączymy aplikacji.
Importy ES6
Importy CommonJS
Z tego też powodu jeśli chcemy, żeby tree-shaking w naszym bundlerze działał poprawnie, musimy się upewnić, że przed procesem tree-shakingu nie ma miejsca proces transpilacji kodu ES6 do kodu ES5 z modułami CommonJS (a to jest dokładnie to, co robi bardzo popularny preset do babela – @babel/preset-env).
Tree-shaking, a biblioteki
Do poprawnego tree-shakingu musimy się też upewnić, że biblioteki, z których korzystamy wykorzystują moduły ES6 i tutaj świetnym przykładem będzie biblioteka lodash, który z nich nie korzysta, na co się natknąłem pracując ostatnio dla Idego w projekcie ScopeOne.
Początkowo importowaliśmy funkcje pomocnicze lodasha w poniższy sposób:
Z czasem okazało mimo korzystania jedynie z funkcji get oraz kilku innych do naszego kodu wynikowego trafiał calutki lodash. Remedium na to zjawisko jest między innymi paczka lodash-es, która jest po prostu kopią biblioteki lodash, ale wyeksportowaną w postaci modułów ES6.
Innym rozwiązaniem byłyby tzw direct imports:
lub użycie babel-plugin-lodash.
Warto dodać, że według https://lodash.com/per-method-packages część współczesnych bundlerów pozwala na obejście się bez wyżej opisanych problemów.
webpack-bundle-analyzer
Jeśli chcesz sprawdzić, czy Twoja aplikacja nie zawiera przypadkiem sporej ilości niepotrzebnego kodu możesz skorzystać z webpack-bundle-analyzer
, czyli pluginu do webpacka, który zwizualizuje pliki wynikowe twojej aplikacji wyszczególniając poszczególne zależności w bardzo przystępny dla oka sposób.
Dzięki temu będziesz mógł:
- sprawdzić co tak naprawdę trafiło do naszego bundle.js,
- zauważyć, które zależności ważą najwięcej,
- zauważyć, które zależności trafiły tam przez przypadek i są zbędne,
- zoptymalizować aplikację poprawiając importy i pozbywając się niepotrzebnego kodu.
Jak widzicie web tree-shaking pozwala w łatwy i szybki sposób zrobić porządek z tym czego już nie chcemy. Zachęcam do wypróbowania i odciążenia swojego kodu!
Zdjęcie główne artykułu pochodzi z unsplash.com.