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 paket ini membuat paket JavaScript Anda 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. Platform ini awalnya dimaksudkan untuk digunakan di luar {i>browser<i} web, terutama untuk aplikasi sisi server.

Dengan CommonJS, Anda dapat menentukan modul, mengekspor fungsi darinya, 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]);

Kemudian, 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 terstandardisasi pada browser pada awal 2010-an, CommonJS menjadi format modul populer untuk library sisi klien JavaScript juga.

Bagaimana pengaruh CommonJS terhadap ukuran paket akhir Anda?

Ukuran aplikasi JavaScript sisi server Anda tidak sepenting di 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 penyebab aplikasi browser lebih lambat.

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

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 bangun 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 pada aset produksi kita.

Sekarang, mari kita ubah format modul menjadi modul ECMAScript lalu 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));

Menggunakan konfigurasi webpack yang sama, kita dapat membangun aplikasi dan membuka file output. Sekarang menjadi 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 trace dari lodash. Lebih jauh lagi, terser (minifier JavaScript yang digunakan webpack) menjadikan fungsi add sebagai inline di console.log.

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

Modul CommonJS lebih sulit dioptimalkan dalam kasus umum karena jauh lebih dinamis daripada modul ES. Agar 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 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 menjadi satu penutupan dan memungkinkan kode Anda memiliki waktu eksekusi yang lebih cepat di browser. Perhatikan contoh berikut:

// 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:

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

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

Perilaku ini diaktifkan secara default untuk modul ES karena modul tersebut 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 saja:

...
(() => {

"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 final berisi beberapa "runtime" webpack: kode yang dimasukkan yang bertanggung jawab untuk mengimpor/mengekspor fungsi dari modul yang dipaketkan. Kali ini, alih-alih menempatkan semua simbol dari utils.js dan index.js dalam namespace yang sama, kita memerlukan fungsi add secara dinamis, saat runtime, menggunakan __webpack_require__.

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 nama simbol yang diekspor pada waktu build, 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 menghilangkannya. Kita juga 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 bersifat dinamis menurut definisi. Misalnya, lokasi impor dalam modul ES selalu berupa literal string, dibandingkan dengan CommonJS, yang berupa ekspresi.

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

Kesimpulan

Agar 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:

  • Menggunakan node-resolve Rollup.js plugin dan setel tanda modulesOnly untuk menentukan bahwa Anda hanya ingin bergantung pada modul ECMAScript.
  • Menggunakan 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.