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 mieć pewność, że narzędzie tworzące pakiet może zoptymalizować aplikację, unikaj stosowania modułów CommonJS i używaj składni modułu ECMAScript w całej aplikacji.

Co to jest CommonJS?

CommonJS to standard z 2009 roku, który ustanowił konwencje dla 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 wskazuje, że rozmiar pakietu JavaScript nadal jest główną przyczyną spowalniania aplikacji w przeglądarkach.

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 we fragmencie kodu powyżej końcowy pakiet powinien zawierać tylko funkcję add, ponieważ jest to jedyny symbol z pola utils.js, który importujesz w polu 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

Pamiętaj, że rozmiar pakietu to 625 KB. Jeśli przyjrzymy się wynikowi, 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ńmy teraz 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);

Natomiast index.js zaimportuje dane z utils.js przy użyciu składni modułu 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 to tylko przykład zabawek. W rzeczywistości różnica w rozmiarze może nie być aż tak duża, ale istnieje spora szansa, że system CommonJS znacznie zwiększy produkcję konstrukcji.

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ę działaniu ModuleConcatenationPlugin w webpack, a następnie omówimy analizę statyczną. Ten wtyczek konkatenuje zakres wszystkich modułów w jedną funkcję zamykającą i pozwala na szybsze wykonywanie 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, pakiet internetowy zmienił nazwę funkcji subtract w 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 zbędne odstępy
  • Umieść kod funkcji add w ciele wywołania funkcji console.log.

Deweloperzy często nazywają to usunięcie nieużywanych importów jako „potrząsanie drzewem. 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 można go było osadzić na tej stronie, 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 działania”: 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())] = () => {  };

Twórca pakietu nie może dowiedzieć się w czasie kompilacji, jaka jest nazwa wyeksportowanego symbolu, ponieważ wymaga to informacji dostępnych tylko w czasie działania i 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 usługi node_modules, Twój łańcuch narzędzi nie będzie w stanie go prawidłowo zoptymalizować.

Trzęsienie się drzew z 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 wtyku. 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.