Cách CommonJS tăng quy mô nhóm

Tìm hiểu cách các mô-đun CommonJS đang ảnh hưởng đến quá trình rung cây của ứng dụng như thế nào

Trong bài đăng này, chúng ta sẽ tìm hiểu CommonJS là gì và lý do khiến các gói JavaScript của bạn lớn hơn mức cần thiết.

Tóm tắt: Để đảm bảo bộ gói có thể tối ưu hoá thành công ứng dụng của bạn, hãy tránh phụ thuộc vào các mô-đun CommonJS và sử dụng cú pháp mô-đun ECMAScript trong toàn bộ ứng dụng của bạn.

CommonJS là gì?

CommonJS là một tiêu chuẩn từ năm 2009, thiết lập các quy ước cho các mô-đun JavaScript. Ban đầu, trình duyệt được thiết kế để sử dụng bên ngoài trình duyệt web, chủ yếu cho các ứng dụng phía máy chủ.

Với CommonJS, bạn có thể xác định các mô-đun, xuất chức năng từ đó và nhập chúng vào các mô-đun khác. Ví dụ: đoạn mã dưới đây xác định một mô-đun xuất 5 hàm: 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]);

Sau đó, một mô-đun khác có thể nhập và sử dụng một số hoặc tất cả các hàm sau:

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

Nếu bạn gọi index.js bằng node, thì số 3 sẽ xuất hiện trong bảng điều khiển.

Do thiếu một hệ thống mô-đun được chuẩn hoá trong trình duyệt vào đầu những năm 2010, CommonJS cũng đã trở thành một định dạng mô-đun phổ biến cho thư viện phía máy khách JavaScript.

CommonJS ảnh hưởng đến kích thước gói cuối cùng của bạn như thế nào?

Kích thước ứng dụng JavaScript phía máy chủ của bạn không quan trọng như trong trình duyệt, đó là lý do tại sao CommonJS không được thiết kế để giảm kích thước gói sản xuất. Đồng thời, phân tích cho thấy rằng kích thước gói JavaScript vẫn là lý do số một khiến các ứng dụng trình duyệt chạy chậm hơn.

Các trình gói và trình thu nhỏ JavaScript (như webpackterser) thực hiện nhiều hoạt động tối ưu hoá để giảm kích thước ứng dụng của bạn. Phân tích ứng dụng của bạn tại thời điểm xây dựng, họ cố gắng xoá mã nguồn mà bạn không sử dụng nhiều nhất có thể.

Ví dụ: trong đoạn mã trên, gói cuối cùng của bạn chỉ nên bao gồm hàm add vì đây là biểu tượng duy nhất từ utils.js mà bạn nhập trong index.js.

Hãy tạo bản dựng ứng dụng bằng cấu hình webpack sau:

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

Ở đây, chúng ta xác định rằng chúng ta muốn sử dụng tính năng tối ưu hoá chế độ phát hành công khai và sử dụng index.js làm điểm truy cập. Sau khi gọi webpack, nếu khám phá kích thước đầu ra, chúng ta sẽ thấy như sau:

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

Lưu ý rằng gói có kích thước 625KB. Nếu xem xét kết quả, chúng ta sẽ thấy tất cả các hàm từ utils.js cùng với rất nhiều mô-đun từ lodash. Mặc dù chúng ta không sử dụng lodash trong index.js, nhưng đây là một phần của kết quả. Việc này làm tăng thêm rất nhiều trọng số cho tài sản chính thức.

Bây giờ, hãy thay đổi định dạng mô-đun thành mô-đun ECMAScript rồi thử lại. Lần này, utils.js sẽ có dạng như sau:

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

index.js sẽ nhập từ utils.js bằng cú pháp mô-đun ECMAScript:

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

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

Bằng cách sử dụng cùng một cấu hình webpack, chúng ta có thể xây dựng ứng dụng và mở tệp đầu ra. Kích thước hiện tại là 40 byte với đầu ra sau:

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

Hãy lưu ý rằng gói cuối cùng sẽ không chứa bất kỳ hàm nào từ utils.js mà chúng ta không sử dụng, cũng như không có dấu vết từ lodash! Hơn nữa, terser (trình rút gọn JavaScript mà webpack sử dụng) đưa hàm add vào console.log.

Một câu hỏi công bằng mà bạn có thể đặt ra là tại sao việc sử dụng CommonJS lại khiến gói đầu ra lớn hơn gần 16.000 lần? Tất nhiên, đây là ví dụ về đồ chơi, trên thực tế, sự chênh lệch về kích thước có thể không lớn đến mức đó, nhưng có khả năng CommonJS sẽ tăng đáng kể trọng lượng cho bản dựng chính thức của bạn.

Mô-đun CommonJS khó tối ưu hoá hơn trong trường hợp chung vì chúng linh hoạt hơn nhiều so với mô-đun ES. Để đảm bảo trình gói và trình rút gọn có thể tối ưu hoá thành công ứng dụng của bạn, hãy tránh phụ thuộc vào các mô-đun CommonJS và sử dụng cú pháp mô-đun ECMAScript trong toàn bộ ứng dụng.

Lưu ý rằng ngay cả khi bạn đang sử dụng các mô-đun ECMAScript trong index.js, nếu mô-đun bạn đang sử dụng là mô-đun CommonJS, thì kích thước gói của ứng dụng sẽ bị ảnh hưởng.

Tại sao CommonJS làm cho ứng dụng của bạn lớn hơn?

Để trả lời câu hỏi này, chúng ta sẽ xem xét hành vi của ModuleConcatenationPlugin trong webpack, sau đó thảo luận về khả năng phân tích tĩnh. Trình bổ trợ này nối phạm vi của tất cả các mô-đun thành một đóng và cho phép mã của bạn có thời gian thực thi nhanh hơn trong trình duyệt. Hãy xem ví dụ:

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

Ở trên, chúng ta có mô-đun ECMAScript mà chúng ta sẽ nhập trong index.js. Chúng ta cũng xác định hàm subtract. Chúng ta có thể tạo dự án bằng cách sử dụng cùng cấu hình webpack như trên, nhưng lần này, chúng ta sẽ tắt tính năng thu nhỏ:

const path = require('path');

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

Hãy xem kết quả được tạo ra:

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

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

Ở kết quả ở trên, tất cả các hàm đều nằm trong cùng một không gian tên. Để ngăn xảy ra xung đột, webpack đã đổi tên hàm subtract trong index.js thành index_subtract.

Nếu một trình rút gọn xử lý mã nguồn ở trên, trình rút gọn sẽ:

  • Xoá các hàm subtractindex_subtract không dùng đến
  • Xóa tất cả nhận xét và khoảng trắng thừa
  • Đặt nội tuyến phần nội dung của hàm add trong lệnh gọi console.log

Thông thường, các nhà phát triển xem đây là việc loại bỏ các mục nhập không được sử dụng vì đây là hành vi lắc cây. Bạn chỉ có thể sử dụng hiệu ứng rung cây vì webpack có thể theo cách tĩnh (tại thời điểm xây dựng) hiểu được những biểu tượng mà chúng tôi đang nhập từ utils.js và những biểu tượng mà gói web xuất ra.

Theo mặc định, hành vi này được bật cho các mô-đun ES vì các mô-đun này có thể phân tích tĩnh hơn so với CommonJS.

Chúng ta hãy cùng xem ví dụ này, nhưng lần này hãy thay đổi utils.js để sử dụng CommonJS thay vì các mô-đun 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]);

Bản cập nhật nhỏ này sẽ thay đổi đáng kể kết quả. Do quá dài để nhúng trên trang này nên tôi chỉ chia sẻ một phần nhỏ:

...
(() => {

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

})();

Xin lưu ý rằng gói cuối cùng chứa một số "thời gian chạy" webpack: mã được chèn chịu trách nhiệm nhập/xuất chức năng từ các mô-đun đi kèm. Lần này, thay vì đặt tất cả các biểu tượng từ utils.jsindex.js trong cùng một không gian tên, chúng ta sẽ yêu cầu hàm add một cách linh động trong thời gian chạy bằng cách sử dụng __webpack_require__.

Điều này là cần thiết vì với CommonJS, chúng ta có thể lấy tên xuất từ một biểu thức tùy ý. Ví dụ: mã bên dưới là một cấu trúc hoàn toàn hợp lệ:

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

Trình đóng gói không có cách nào để biết tên của biểu tượng đã xuất tại thời điểm xây dựng, vì điều này đòi hỏi thông tin chỉ có trong thời gian chạy, trong bối cảnh trình duyệt của người dùng.

Bằng cách này, trình rút gọn không thể hiểu chính xác index.js sử dụng gì trong các phần phụ thuộc, do đó không thể loại bỏ tác vụ này. Chúng tôi cũng sẽ quan sát thấy hành vi hoàn toàn tương tự cho các mô-đun của bên thứ ba. Nếu chúng ta nhập mô-đun CommonJS từ node_modules, thì chuỗi công cụ bản dựng của bạn sẽ không thể tối ưu hoá mô-đun đó đúng cách.

Lắc cây với CommonJS

Sẽ khó hơn nhiều khi phân tích các mô-đun CommonJS vì chúng có tính động theo định nghĩa. Ví dụ: vị trí nhập trong các mô-đun ES luôn là một giá trị cố định kiểu chuỗi, so với CommonJS, trong đó nó là một biểu thức.

Trong một số trường hợp, nếu thư viện bạn đang sử dụng tuân theo các quy ước cụ thể về cách sử dụng CommonJS, thì bạn có thể xoá các tệp xuất không dùng đến trong thời gian xây dựng bằng cách sử dụng trình bổ trợ webpack của bên thứ ba. Mặc dù trình bổ trợ này hỗ trợ thêm kỹ thuật rung cây, nhưng trình bổ trợ này không bao gồm tất cả các cách mà các phần phụ thuộc của bạn có thể sử dụng CommonJS. Điều này có nghĩa là bạn sẽ không nhận được những đảm bảo giống như đối với các mô-đun ES. Ngoài ra, việc này còn làm tăng thêm chi phí trong quá trình xây dựng, ngoài hành vi mặc định của webpack.

Kết luận

Để đảm bảo bộ gói có thể tối ưu hoá thành công ứng dụng của bạn, hãy tránh phụ thuộc vào các mô-đun CommonJS và sử dụng cú pháp mô-đun ECMAScript trong toàn bộ ứng dụng của bạn.

Dưới đây là một vài mẹo hữu ích để xác minh rằng bạn đang đi trên lộ trình tối ưu:

  • Sử dụng tính năng node-resolve của Rollup.js trình bổ trợ và đặt cờ modulesOnly để chỉ định rằng bạn chỉ muốn phụ thuộc vào các mô-đun ECMAScript.
  • Sử dụng gói is-esm để xác minh rằng gói npm sử dụng các mô-đun ECMAScript.
  • Nếu đang sử dụng Angular, theo mặc định, bạn sẽ nhận được cảnh báo nếu phụ thuộc vào các mô-đun không thay đổi được dạng cây.