Bagaimana CommonJS membuat paket Anda lebih besar

Pelajari pengaruh modul CommonJS terhadap tree-shaking aplikasi Anda

Dalam postingan ini, kita akan melihat apa itu CommonJS dan mengapa membuat paket JavaScript Anda jadi lebih besar dari yang diperlukan.

Ringkasan: Untuk memastikan pemaket berhasil mengoptimalkan aplikasi Anda, hindari ketergantungan pada modul CommonJS, dan gunakan sintaksis modul ECMAScript di seluruh aplikasi Anda.

Apa itu CommonJS?

CommonJS adalah standar dari tahun 2009 yang menetapkan konvensi untuk modul JavaScript. Awalnya, layanan ini dimaksudkan untuk digunakan di luar browser web, terutama untuk aplikasi sisi server.

Dengan CommonJS Anda dapat menentukan modul, mengekspor fungsi dari modul tersebut, dan mengimpornya di modul lain. Misalnya, cuplikan di bawah menentukan modul yang mengekspor lima fungsi: add, subtract, multiply, divide, dan 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]);

Selanjutnya, modul lain dapat mengimpor dan menggunakan beberapa atau semua fungsi ini:

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

Memanggil index.js dengan node akan menghasilkan angka 3 di konsol.

Karena kurangnya sistem modul standar dalam browser pada awal 2010-an, CommonJS juga menjadi format modul yang populer untuk library sisi klien JavaScript.

Bagaimana pengaruh CommonJS terhadap ukuran paket akhir Anda?

Ukuran aplikasi JavaScript sisi server Anda tidak sepenting ukuran browser, itu sebabnya CommonJS tidak dirancang dengan mempertimbangkan ukuran paket produksi. Pada saat yang sama, analisis menunjukkan bahwa ukuran paket JavaScript masih menjadi alasan nomor satu untuk membuat aplikasi browser menjadi lebih lambat.

Pemaket dan minifier JavaScript, seperti webpack dan terser, melakukan pengoptimalan yang berbeda untuk mengurangi ukuran aplikasi. Saat menganalisis aplikasi pada waktu build, alat ini mencoba menghapus sebanyak mungkin dari kode sumber yang tidak Anda gunakan.

Misalnya, dalam cuplikan di atas, paket akhir Anda hanya boleh menyertakan fungsi add karena ini adalah satu-satunya simbol dari utils.js yang Anda impor di index.js.

Mari kita build aplikasi menggunakan konfigurasi webpack berikut:

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

Di sini kita menentukan bahwa kita ingin menggunakan pengoptimalan mode produksi dan menggunakan index.js sebagai titik entri. Setelah memanggil webpack, jika kita menjelajahi ukuran output, kita akan melihat sesuatu seperti ini:

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

Perhatikan bahwa paket berukuran 625 KB. Jika melihat output, kita akan menemukan semua fungsi dari utils.js ditambah banyak modul dari lodash. Meskipun kita tidak menggunakan lodash di index.js, ini adalah bagian dari output, yang menambahkan banyak bobot ekstra ke aset produksi kita.

Sekarang mari kita ubah format modul menjadi modul ECMAScript dan coba lagi. Kali ini, utils.js akan terlihat seperti ini:

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

Dan index.js akan mengimpor dari utils.js menggunakan sintaksis modul ECMAScript:

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

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

Dengan menggunakan konfigurasi webpack yang sama, kita dapat mem-build aplikasi dan membuka file output. Sekarang ukurannya 40 byte dengan output berikut:

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

Perhatikan bahwa paket akhir tidak berisi fungsi apa pun dari utils.js yang tidak digunakan, dan tidak ada rekaman aktivitas dari lodash. Selain itu, terser (minifier JavaScript yang digunakan webpack) membuat fungsi add menjadi inline di console.log.

Pertanyaan wajar yang mungkin Anda ajukan adalah, mengapa penggunaan CommonJS menyebabkan bundle output hampir 16.000 kali lebih besar? Tentu saja, ini adalah contoh mainan, pada kenyataannya, perbedaan ukurannya mungkin tidak sebesar itu, tetapi kemungkinan bahwa CommonJS menambah bobot yang signifikan pada build produksi Anda.

Modul CommonJS lebih sulit dioptimalkan secara umum karena jauh lebih dinamis daripada modul ES. Untuk memastikan pemaket dan minifier Anda berhasil mengoptimalkan aplikasi, hindari ketergantungan pada modul CommonJS, dan gunakan sintaksis modul ECMAScript di seluruh aplikasi Anda.

Perhatikan bahwa meskipun Anda menggunakan modul ECMAScript di index.js, jika modul yang Anda gunakan adalah modul CommonJS, ukuran paket aplikasi Anda akan terpengaruh.

Mengapa CommonJS membuat aplikasi Anda menjadi lebih besar?

Untuk menjawab pertanyaan ini, kita akan melihat perilaku ModuleConcatenationPlugin di webpack dan, setelah itu, membahas kemampuan analisis statis. Plugin ini menggabungkan cakupan semua modul Anda menjadi satu penutupan dan memungkinkan kode memiliki waktu eksekusi yang lebih cepat di browser. Perhatikan contohnya:

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

Di atas, kita memiliki modul ECMAScript, yang kita impor di index.js. Kita juga menentukan fungsi subtract. Kita dapat membuat project menggunakan konfigurasi webpack yang sama seperti di atas, tetapi kali ini, kita akan menonaktifkan minimalisasi:

const path = require('path');

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

Mari kita lihat output yang dihasilkan:

/******/ (() => { // 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));**

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

Pada output di atas, semua fungsi berada di dalam namespace yang sama. Untuk mencegah konflik, webpack mengganti nama fungsi subtract di index.js menjadi index_subtract.

Jika minifier memproses kode sumber di atas, minifier akan:

  • Menghapus fungsi subtract dan index_subtract yang tidak digunakan
  • Menghapus semua komentar dan spasi kosong yang berlebihan
  • Sejajarkan isi fungsi add dalam panggilan console.log

Sering kali developer menyebut penghapusan impor yang tidak digunakan ini sebagai tree-shaking. Tree-shaking hanya dimungkinkan karena webpack dapat secara statis (pada waktu build) memahami simbol mana yang kita impor dari utils.js dan simbol apa yang diekspornya.

Perilaku ini diaktifkan secara default untuk modul ES karena modul ini lebih dapat dianalisis secara statis, dibandingkan dengan CommonJS.

Mari kita lihat contoh yang sama persis, tetapi kali ini ubah utils.js untuk menggunakan CommonJS, bukan modul ES:

// 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]);

Update kecil ini akan mengubah output secara signifikan. Karena terlalu panjang untuk disematkan di halaman ini, saya hanya membagikan sebagian kecil:

...
(() => {

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

})();

Perhatikan bahwa paket akhir berisi beberapa webpack "runtime": kode yang dimasukkan dan bertanggung jawab untuk mengimpor/mengekspor fungsi dari modul yang dipaketkan. Kali ini, alih-alih menempatkan semua simbol dari utils.js dan index.js di dalam namespace yang sama, kita memerlukan fungsi add secara dinamis saat runtime menggunakan __webpack_require__.

Hal ini diperlukan karena dengan CommonJS kita bisa mendapatkan nama ekspor dari ekspresi arbitrer. Misalnya, kode di bawah ini adalah konstruksi yang benar-benar valid:

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

Tidak ada cara bagi pemaket untuk mengetahui pada waktu build nama simbol yang diekspor karena hal ini memerlukan informasi yang hanya tersedia saat runtime, dalam konteks browser pengguna.

Dengan cara ini, minifier tidak dapat memahami apa yang sebenarnya digunakan index.js dari dependensinya sehingga tidak dapat mengguncangkannya. Kita akan mengamati perilaku yang sama persis untuk modul pihak ketiga. Jika kita mengimpor modul CommonJS dari node_modules, toolchain build Anda tidak akan dapat mengoptimalkannya dengan benar.

Tree-shaking dengan CommonJS

Jauh lebih sulit untuk menganalisis modul CommonJS karena modul ini dinamis menurut definisi. Misalnya, lokasi impor dalam modul ES selalu berupa literal string, dibandingkan dengan CommonJS, yang merupakan ekspresi.

Dalam beberapa kasus, jika library yang Anda gunakan mengikuti konvensi khusus tentang cara penggunaan CommonJS, Anda dapat menghapus ekspor yang tidak digunakan pada waktu build menggunakan plugin webpack pihak ketiga. Meskipun plugin ini menambahkan dukungan untuk tree-shaking, plugin ini tidak mencakup semua cara yang dapat dilakukan dependensi Anda menggunakan CommonJS. Ini berarti Anda tidak mendapatkan jaminan yang sama seperti modul ES. Selain itu, ini menambahkan biaya tambahan sebagai bagian dari proses build Anda selain perilaku webpack default.

Kesimpulan

Untuk memastikan pemaket berhasil mengoptimalkan aplikasi Anda, hindari ketergantungan pada modul CommonJS, dan gunakan sintaksis modul ECMAScript di seluruh aplikasi Anda.

Berikut adalah beberapa tips yang dapat ditindaklanjuti untuk memverifikasi bahwa Anda berada di jalur yang optimal:

  • Gunakan plugin node-resolve Rollup.js dan tetapkan flag modulesOnly untuk menentukan bahwa Anda hanya ingin bergantung pada modul ECMAScript.
  • Gunakan paket is-esm untuk memverifikasi bahwa paket npm menggunakan modul ECMAScript.
  • Jika menggunakan Angular, secara default Anda akan mendapatkan peringatan jika bergantung pada modul yang tidak dapat di-tree-shaking.