Jak CommonJS zwiększa Twoje pakiety

Dowiedz się, jak moduły CommonJS wpływają na potrząsanie drzewem w aplikacji

Z tego posta dowiesz się, czym jest CommonJS i dlaczego powoduje, że Twoje pakiety JavaScript 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ł on przeznaczony do użytku poza przeglądarką, głównie w aplikacjach po stronie serwera.

CommonJS pozwala definiować moduły, eksportować z nich funkcje i importować je w innych modułach. Na przykład ten fragment kodu definiuje moduł, który eksportuje 5 funkcji: add, subtract, multiply, divide i max:

// 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 w innym module będzie można importować i używać niektórych lub wszystkich tych funkcji:

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

Wywołanie index.js za pomocą funkcji node spowoduje wyświetlenie w konsoli liczby 3.

Ze względu na brak ujednoliconego systemu modułów w przeglądarce na początku 2010 r. format CommonJS stał się popularnym formatem modułów w przypadku bibliotek JavaScript po stronie klienta.

Jak CommonJS wpływa na ostateczny rozmiar pakietu?

Rozmiar aplikacji JavaScript po stronie serwera nie ma takiego znaczenia jak rozmiar przeglądarki, dlatego CommonJS nie zaprojektowano tak, by zmniejszyć rozmiar pakietu produkcyjnego. Jednocześnie analiza wskazuje, że rozmiar pakietu JavaScript nadal jest główną przyczyną spowalniania aplikacji w przeglądarkach.

Pakiety pakietów i minifikatory JavaScript, np. webpack i terser, przeprowadzają różne optymalizacje, aby zmniejszyć rozmiar aplikacji. Analizują oni Twoją aplikację w czasie kompilacji i starają się usunąć z nieużywanego przez Ciebie kodu źródłowego jak najwięcej.

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 importowany w polu index.js.

Skompilujemy aplikację, korzystając z 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żyć optymalizacji w trybie produkcyjnym i użyć index.js jako punktu wejścia. Jeśli po wywołaniu funkcji webpack sprawdzimy rozmiar danych wyjściowych, 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ę wynikom, zobaczymy wszystkie funkcje z tabeli utils.js oraz wiele modułów z tabeli lodash. Chociaż w index.js nie używamy atrybutu lodash, jest on częścią danych wyjściowych, 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 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));

Przy użyciu tej samej konfiguracji webpack możemy skompilować aplikację i otworzyć plik wyjściowy. Ma teraz 40 bajtów i następuje następujące wyjściowe:

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

Zwróć uwagę, że ostateczna wersja pakietu nie zawiera żadnej z nieużywanych przez nas funkcji z tabeli utils.js, ani nie ma żadnego śladu z pliku lodash. Co więcej, terser (minifikator JavaScriptu używany w webpack) wbudował funkcję add w console.log.

Możesz zadać sobie pytanie, dlaczego korzystanie z CommonJS powoduje,że pakiet wyjściowy jest prawie 16 000 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.

W ogólnym przypadku moduły CommonJS są trudniejsze do optymalizacji, ponieważ są one znacznie bardziej dynamiczne niż moduły ES. Aby mieć pewność, że kreator pakietów i minifikator poprawnie zoptymalizuje aplikację, unikaj stosowania modułów CommonJS i używaj składni modułu ECMAScript w całej aplikacji.

Zwróć uwagę, że nawet wtedy, gdy w index.js używasz modułów ECMAScript, ucierpi rozmiar pakietu aplikacji, jeśli używany jest moduł CommonJS.

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ą. Ta wtyczka łączy zakres wszystkich modułów w jedno zamknięcie, co pozwala skrócić czas wykonywania 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 języku index.js. Definiujemy również funkcję subtract. Możemy skompilować projekt przy użyciu takiej samej konfiguracji webpack jak powyżej, ale tym razem wyłączymy minimalizację:

const path = require('path');

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

Spójrzmy na uzyskane dane:

/******/ (() => { // 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 danych wyjściowych powyżej wszystkie funkcje znajdują się w tej samej przestrzeni nazw. Aby zapobiec kolizjom, pakiet internetowy zmienił nazwę funkcji subtract w index.js na index_subtract.

Jeśli minifikator przetworzy powyższy kod źródłowy:

  • Usuń nieużywane funkcje subtract i index_subtract
  • Usuń wszystkie komentarze i zbędne odstępy
  • Wbudowana treść funkcji add w wywołaniu console.log

Deweloperzy często nazywają to usunięcie nieużywanych importów jako „potrząsanie drzewem. Drżenie drzew było możliwe tylko dlatego, że pakiet internetowy był w stanie statycznie (w momencie tworzenia) określić, które symbole są importowane z utils.js i jakie symbole eksportuje.

To zachowanie jest domyślnie włączone w przypadku modułów ES, ponieważ można je analizować statycznie bardziej niż CommonJS.

Przyjrzyjmy się dokładnie temu samemu przykładowi, ale tym razem w utils.js zamiast modułów ES zostanie użyty moduł 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 aktualizacja znacząco zmieni wyniki. Ponieważ jest on za długi, aby go umieścić na tej stronie, udostępniam tylko jego niewielką 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));

})();

Zwróć uwagę, że końcowy pakiet zawiera „środowisko wykonawcze” webpack: wstrzyknięty kod odpowiedzialny za importowanie/eksportowanie funkcji z pakietów modułów. Tym razem zamiast umieszczać wszystkie symbole z utils.js i index.js w tej samej przestrzeni nazw, wymagamy dynamicznego, w czasie działania funkcji add z użyciem __webpack_require__.

Jest to konieczne, ponieważ dzięki CommonJS możemy uzyskać nazwę eksportu z dowolnego wyrażenia. Bezwzględnie prawidłowy jest na przykład poniższy kod:

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.

Dzięki temu minifikator nie może zrozumieć, co dokładnie index.js wykorzystuje w zależnościach, i nie będzie w stanie poruszyć drzewem. To samo obserwujemy również 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

Analiza modułów CommonJS jest o wiele trudniejsza, ponieważ są one z definicji dynamiczne. Na przykład lokalizacja importu w modułach ES jest zawsze literałem łańcuchowym w porównaniu do formatu CommonJS, gdzie jest to wyrażenie.

W niektórych przypadkach, jeśli używana przez Ciebie biblioteka podlega określonym konwencjom dotyczącym korzystania z CommonJS, możesz usunąć nieużywane eksporty w czasie kompilacji, korzystając z wtyczki innej firmy webpack. Chociaż ta wtyczka obsługuje wstrząsanie drzew, nie uwzględnia wszystkich różnych sposobów wykorzystania CommonJS przez zależności. Oznacza to, że nie otrzymasz takich samych gwarancji co w przypadku modułów ES. Poza tym domyślne działanie webpack wiąże się z dodatkowymi kosztami w ramach procesu kompilacji.

Podsumowanie

Aby umożliwić systemowi optymalizowanie aplikacji, unikaj stosowania modułów CommonJS i używaj składni modułu ECMAScript w całej aplikacji.

Oto kilka praktycznych wskazówek, które pomogą Ci sprawdzić, czy jesteś na optymalnej ścieżce:

  • Użyj funkcji node-resolve Rollup.js wtyczki i ustawić flagę modulesOnly, aby wskazać, że witryna ma się opierać wyłącznie na modułach ECMAScript.
  • Użyj pakietu is-esm aby sprawdzić, czy pakiet npm używa modułów ECMAScript.
  • Jeśli używasz Angular, domyślnie otrzymasz ostrzeżenie, jeśli korzystasz z modułów, które nie są podatne na potrząsanie drzewem.