Jak CommonJS zwiększa Twoje pakiety

Dowiedz się, jak moduły CommonJS wpływają na usuwanie drzewa w aplikacji

W tym poście omówimy, czym jest CommonJS i dlaczego powoduje, że paczki JavaScriptu są większe niż to konieczne.

Podsumowanie: aby zapewnić prawidłowe działanie optymalizatora w przypadku aplikacji, unikaj zależności od modułów CommonJS i używaj w całej aplikacji składni modułów ECMAScript.

CommonJS to standard z 2009 roku, który określa konwencje dotyczące modułów JavaScript. Początkowo była przeznaczona do użytku poza przeglądarką internetową, głównie w przypadku aplikacji po stronie serwera.

Dzięki CommonJS możesz definiować moduły, eksportować z nich funkcje i importować je do innych modułów. Na przykład poniższy fragment kodu definiuje moduł, który eksportuje 5 funkcji: add, subtract, multiply, dividemax:

// utils.js
const { maxBy } = require('lodash-es');
const fns = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b,
  max: arr => maxBy(arr)
};

Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);

Później inny moduł może importować i wykorzystywać niektóre lub wszystkie z tych funkcji:

// index.js
const { add } = require('./utils.js');
console.log(add(1, 2));

Wywołanie funkcji index.js z argumentem node spowoduje wyświetlenie w konsoli numeru 3.

Na początku lat 2010. z powodu braku ustandaryzowanego systemu modułów w przeglądarce format CommonJS stał się popularnym formatem modułów dla bibliotek po stronie klienta JavaScript.

Jak CommonJS wpływa na ostateczny rozmiar pakietu?

Rozmiar aplikacji JavaScript po stronie serwera nie jest tak istotny jak w przeglądarce, dlatego CommonJS nie został zaprojektowany z myślą o zmniejszaniu rozmiaru pakietu produkcyjnego. Jednocześnie analiza pokazuje, że rozmiar pakietu JavaScript jest nadal głównym powodem spowolnienia działania aplikacji w przeglądarce.

Pakiety JavaScript i programy do minifikacji, takie jak webpackterser, wykonują różne optymalizacje, aby zmniejszyć rozmiar aplikacji. Analizując aplikację w momencie kompilacji, starają się usunąć jak najwięcej kodu źródłowego, którego nie używasz.

Na przykład w fragmentie kodu powyżej w ostatecznym pakiecie powinno się znaleźć tylko polecenie add, ponieważ jest to jedyny symbol z funkcji utils.js, który importujesz do funkcji index.js.

Skompilujmy aplikację przy użyciu tej konfiguracji webpack:

const path = require('path');
module.exports = {
  entry: 'index.js',
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'production',
};

Tutaj określamy, że chcemy używać optymalizacji w trybie produkcyjnym i użyć index.js jako punktu wejścia. Po wywołaniu funkcji webpack i sprawdzeniu rozmiaru wyjścia zobaczysz coś takiego:

$ cd dist && ls -lah
625K Apr 13 13:04 out.js

Zwróć uwagę, że pakiet ma rozmiar 625 KB. Jeśli przyjrzymy się wynikom, zobaczymy wszystkie funkcje z utils.js oraz wiele modułów z lodash. Chociaż nie używamy lodashindex.js, jest on częścią wyjścia, co znacznie zwiększa wagę naszych zasobów produkcyjnych.

Zmień format modułu na moduły ECMAScript i spróbuj ponownie. Tym razem utils.js będzie wyglądać tak:

export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;

import { maxBy } from 'lodash-es';

export const max = arr => maxBy(arr);

index.js importuje z utils.js za pomocą składni ECMAScript:

import { add } from './utils.js';

console.log(add(1, 2));

Za pomocą tej samej konfiguracji webpack możemy skompilować aplikację i otworzyć plik wyjściowy. Teraz ma on 40 bajtów i wyświetla następujące dane wyjściowe:

(()=>{"use strict";console.log(1+2)})();

Pamiętaj, że ostateczny pakiet nie zawiera żadnych funkcji z poziomu utils.js, których nie używamy, i nie ma śladu lodash. Co więcej, terser (minifikator JavaScriptu używany przez webpack) wstawił funkcję add w pliku console.log.

Możesz się zastanawiać, dlaczego korzystanie z CommonJS powoduje,że pakiet wyjściowy jest prawie 16 tysięcy razy większy. Oczywiście jest to przykład, w którym rozmiary są umowne. W rzeczywistości różnica może nie być tak duża, ale istnieje duże prawdopodobieństwo, że CommonJS znacznie zwiększy rozmiar wersji produkcyjnej.

Moduły CommonJS są ogólnie trudniejsze do optymalizacji, ponieważ są znacznie bardziej dynamiczne niż moduły ES. Aby zapewnić prawidłowe działanie optymalizatora i kompresora w przypadku aplikacji, unikaj zależności od modułów CommonJS i używaj w całej aplikacji składni modułów ECMAScript.

Pamiętaj, że nawet jeśli używasz w index.js modułów ECMAScript, jeśli moduł, którego używasz, jest modułem CommonJS, rozmiar pakietu aplikacji będzie większy.

Dlaczego CommonJS zwiększa rozmiar aplikacji?

Aby odpowiedzieć na to pytanie, przyjrzymy się zachowaniu ModuleConcatenationPluginwebpack, a potem omówimy stałą analizowalność. Ten wtyczek konkatenuje zakres wszystkich modułów w jedną funkcję zamykającą i pozwala na szybsze wykonanie kodu w przeglądarce. Przeanalizujmy poniższy przykład:

// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from './utils.js';
const subtract = (a, b) => a - b;

console.log(add(1, 2));

Powyżej mamy moduł ECMAScript, który importujemy w index.js. Definiujemy też funkcję subtract. Możemy skompilować projekt, używając tej samej konfiguracji webpack co powyżej, ale tym razem wyłączymy minifikację:

const path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    minimize: false
  },
  mode: 'production',
};

Zobaczmy uzyskane dane wyjściowe:

/******/ (() => { // webpackBootstrap
/******/    "use strict";

// CONCATENATED MODULE: ./utils.js**
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

// CONCATENATED MODULE: ./index.js**
const index_subtract = (a, b) => a - b;**
console.log(add(1, 2));**

/******/ })();

W wyniku powyżej wszystkie funkcje znajdują się w tym samym zakresie nazw. Aby zapobiec kolizjom, webpack zmienił nazwę funkcji subtract w pliku index.js na index_subtract.

Jeśli narzędzie do kompresji przetworzy powyższy kod źródłowy, wykona te czynności:

  • Usuń nieużywane funkcje subtract i index_subtract
  • Usuń wszystkie komentarze i niepotrzebne spacje.
  • Umieść kod funkcji add w wywołaniu funkcji console.log.

Często usuwanie nieużywanych importów nazywane jest „tree-shakingiem”. Wycinanie drzewa było możliwe tylko dlatego, że webpack był w stanie statycznie (w czasie kompilacji) ustalić, które symbole importujemy z utils.js i które symbole eksportuje.

To zachowanie jest domyślnie włączone w przypadku modułów ES, ponieważ są one bardziej podatne na analizę statyczną niż CommonJS.

Przyjrzyjmy się dokładnie temu samemu przykładowi, ale tym razem zamiast modułów ES użyj utils.js CommonJS:

// utils.js
const { maxBy } = require('lodash-es');

const fns = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b,
  max: arr => maxBy(arr)
};

Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);

Ta niewielka zmiana znacznie zmieni wynik. Ponieważ film jest zbyt długi, aby go tutaj osadzić, udostępniam tylko jego część:

...
(() => {

"use strict";
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (a, b) => a - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .add */ .IH)(1, 2));

})();

Pamiętaj, że ostateczny pakiet zawiera webpack „czas wykonywania”: wstrzyknięty kod odpowiedzialny za importowanie i eksportowanie funkcji z modułów w pakiecie. Tym razem zamiast umieszczać wszystkie symbole z utils.jsindex.js w tej samej przestrzeni nazw, dynamicznie żądamy w czasie wykonywania funkcji add, która używa __webpack_require__.

Jest to konieczne, ponieważ za pomocą CommonJS możemy uzyskać nazwę eksportu z dowolnego wyrażenia. Na przykład ten kod jest całkowicie prawidłową konstrukcją:

module.exports[localStorage.getItem(Math.random())] = () => {  };

W czasie kompilacji pakiet nie może znać nazwy wyeksportowanego symbolu, ponieważ wymaga to informacji, które są dostępne tylko w czasie wykonywania w kontekście przeglądarki użytkownika.

W ten sposób narzędzie do kompresji nie jest w stanie zrozumieć, których dokładnie zależności z elementu index.js używa, więc nie może go usunąć. To samo zachowanie zaobserwujemy też w przypadku modułów innych firm. Jeśli zaimportujemy moduł CommonJS z node_modules, Twoje narzędzie do kompilacji nie będzie mogło go prawidłowo zoptymalizować.

Tree-shaking za pomocą CommonJS

Moduły CommonJS są znacznie trudniejsze do analizowania, ponieważ są z definicji dynamiczne. Na przykład lokalizacja importu w modułach ES jest zawsze ciągiem znaków, w odróżnieniu od CommonJS, gdzie jest to wyrażenie.

W niektórych przypadkach, jeśli biblioteka, której używasz, przestrzega określonych konwencji dotyczących używania CommonJS, możesz usunąć nieużywane eksporty w momencie kompilacji, korzystając z zewnętrznego webpack plugin. Chociaż ta wtyczka dodaje obsługę usuwania zbędących fragmentów kodu, nie obejmuje wszystkich sposobów, w jakie zależności mogą używać CommonJS. Oznacza to, że nie otrzymujesz takich samych gwarancji jak w przypadku modułów ES. Dodatkowo powoduje dodatkowe koszty w ramach procesu kompilacji, które są nakładane na domyślne zachowanie webpack.

Podsumowanie

Aby zapewnić optymalizację aplikacji przez pakiet, unikaj zależności od modułów CommonJS i używaj w całej aplikacji składni modułu ECMAScript.

Oto kilka praktycznych wskazówek, które pomogą Ci sprawdzić, czy podążasz optymalną ścieżką:

  • Użyj wtyczki node-resolve z Rollup.js i ustaw flagę modulesOnly, aby określić, że chcesz używać tylko modułów ECMAScript.
  • Użyj pakietu is-esm, aby sprawdzić, czy pakiet npm używa modułów ECMAScript.
  • Jeśli używasz Angulara, w przypadku zależności od modułów, które nie mogą być usuwane z drzewa, otrzymasz ostrzeżenie.