Bagaimana CommonJS membuat paket Anda lebih besar

Pelajari pengaruh modul CommonJS terhadap tree-shaking aplikasi Anda

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

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

CommonJS adalah standar dari tahun 2009 yang menetapkan konvensi untuk modul JavaScript. Awalnya, API 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]);

Nanti, 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 output nomor 3 di konsol.

Karena tidak adanya sistem modul standar di browser pada awal tahun 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 sekritis di browser, itulah sebabnya CommonJS tidak dirancang dengan mempertimbangkan pengurangan ukuran paket produksi. Pada saat yang sama, analisis menunjukkan bahwa ukuran paket JavaScript masih menjadi alasan utama aplikasi browser menjadi lebih lambat.

Penggabung dan pengoptimal JavaScript, seperti webpack dan terser, melakukan pengoptimalan yang berbeda untuk mengurangi ukuran aplikasi Anda. Dengan menganalisis aplikasi Anda pada waktu build, penggabungan dan pengoptimal JavaScript 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, lodash adalah bagian dari output, yang menambahkan banyak beban tambahan 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 diimpor 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 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 kita gunakan, dan tidak ada jejak dari lodash. Lebih jauh lagi, terser (minifier JavaScript yang digunakan webpack) menyisipkan fungsi add di console.log.

Pertanyaan yang wajar untuk Anda ajukan adalah, mengapa penggunaan CommonJS menyebabkan paket output hampir 16.000 kali lebih besar? Tentu saja, ini adalah contoh mainan, pada kenyataannya, perbedaan ukuran mungkin tidak terlalu besar, tetapi kemungkinan besar CommonJS menambahkan beban yang signifikan ke build produksi Anda.

Modul CommonJS lebih sulit dioptimalkan secara umum karena jauh lebih dinamis daripada modul ES. Untuk memastikan bundler dan minifier berhasil mengoptimalkan aplikasi Anda, hindari bergantung 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 Anda 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 mem-build project menggunakan konfigurasi webpack yang sama seperti di atas, tetapi kali ini, kita akan menonaktifkan minifikasi:

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

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

Dalam 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 memproses kode sumber di atas, minifier akan:

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

Sering kali developer menyebut penghapusan impor yang tidak digunakan sebagai tree shaking. Penghapusan hierarki hanya dapat dilakukan 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 lebih mudah 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]);

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

...
(() => {

"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 "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__.

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

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

Tidak ada cara bagi bundler 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 melakukan tree-shake. 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

Modul CommonJS jauh lebih sulit dianalisis karena bersifat dinamis menurut definisinya. Misalnya, lokasi impor di modul ES selalu berupa string literal, dibandingkan dengan CommonJS, yang merupakan ekspresi.

Dalam beberapa kasus, jika library yang Anda gunakan mengikuti konvensi tertentu tentang cara menggunakan 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 digunakan dependensi Anda untuk menggunakan CommonJS. Artinya, Anda tidak mendapatkan jaminan yang sama seperti dengan modul ES. Selain itu, tindakan ini akan menambahkan biaya tambahan sebagai bagian dari proses build Anda di atas perilaku webpack default.

Kesimpulan

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

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

  • Gunakan plugin node-resolve Rollup.js dan tetapkan tanda 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-shake tree.