วิธีที่ CommonJS ทำแพ็กเกจของคุณให้มีขนาดใหญ่ขึ้น

ดูว่าโมดูล CommonJS ส่งผลต่อการสั่นไหวของแอปอย่างไร

ในโพสต์นี้ เราจะมาดูกันว่า CommonJS คืออะไรและทำไมทำให้ Bundle JavaScript มีขนาดใหญ่กว่าที่จำเป็น

สรุป: โปรดหลีกเลี่ยงการใช้งานโมดูล CommonJS และใช้ไวยากรณ์โมดูล ECMAScript กับแอปพลิเคชันทั้งหมด เพื่อให้มั่นใจว่า Bundler จะเพิ่มประสิทธิภาพแอปพลิเคชันได้สําเร็จ

CommonJS คืออะไร

CommonJS เป็นมาตรฐานตั้งแต่ปี 2009 ที่กําหนดแบบแผนสําหรับโมดูล JavaScript ในตอนแรก Google Analytics 4 มีจุดประสงค์สำหรับการใช้นอกเว็บเบราว์เซอร์ ใช้สำหรับแอปพลิเคชันฝั่งเซิร์ฟเวอร์เป็นหลัก

เมื่อใช้ CommonJS คุณจะกำหนดโมดูล ส่งออกฟังก์ชันจากโมดูล และนำเข้าโมดูลอื่นๆ ได้ ตัวอย่างเช่น ข้อมูลโค้ดด้านล่างกำหนดโมดูลที่ส่งออกฟังก์ชัน 5 รายการ ได้แก่ add, subtract, multiply, divide และ 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]);

หลังจากนี้ โมดูลอื่นสามารถนำเข้าและใช้ฟังก์ชันต่อไปนี้บางส่วนหรือทั้งหมด:

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

การเรียกใช้ index.js ด้วย node จะแสดงหมายเลข 3 ในคอนโซล

เนื่องจากไม่มีระบบโมดูลมาตรฐานในเบราว์เซอร์ในช่วงต้นปี 2010 CommonJS จึงกลายเป็นรูปแบบโมดูลที่ได้รับความนิยมสำหรับไลบรารีฝั่งไคลเอ็นต์ของ JavaScript ด้วย

CommonJS ส่งผลต่อขนาดแพ็กเกจสุดท้ายอย่างไร

ขนาดแอปพลิเคชัน JavaScript ฝั่งเซิร์ฟเวอร์ไม่สำคัญเท่าในเบราว์เซอร์ ดังนั้น CommonJS จึงไม่ออกแบบโดยคำนึงถึงการลดขนาดชุดเวอร์ชันที่ใช้งานจริง ในขณะเดียวกัน การวิเคราะห์ก็แสดงให้เห็นว่าขนาดกลุ่ม JavaScript ยังคงเป็นเหตุผลอันดับ 1 ที่ทำให้แอปเบราว์เซอร์ทำงานช้าลง

โปรแกรม Bundle และตัวลดขนาดของ JavaScript เช่น webpack และ terser จะทำการเพิ่มประสิทธิภาพแบบต่างๆ เพื่อลดขนาดของแอป เมื่อวิเคราะห์แอปพลิเคชัน ณ เวลาสร้าง โค้ดเหล่านี้จะพยายามนำซอร์สโค้ดที่คุณไม่ได้ใช้ออกไปให้ได้มากที่สุด

เช่น ในข้อมูลโค้ดด้านบน กลุ่มสุดท้ายควรมีเฉพาะฟังก์ชัน add เพราะนี่คือสัญลักษณ์เดียวจาก utils.js ที่คุณนำเข้าใน index.js

มาสร้างแอปโดยใช้การกำหนดค่าของ webpack ต่อไปนี้กัน

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

ในที่นี้เราระบุว่าต้องการใช้การเพิ่มประสิทธิภาพโหมดการใช้งานจริง และใช้ index.js เป็นจุดแรกเข้า หลังจากเรียกใช้ webpack หากเราสำรวจขนาดเอาต์พุต คุณจะเห็นผลลัพธ์ดังนี้

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

จะเห็นว่าแพ็กเกจมีขนาด 625 KB หากดูที่เอาต์พุต เราจะพบฟังก์ชันทั้งหมดจาก utils.js รวมถึงโมดูลต่างๆ จาก lodashมากมาย แม้ว่าเราจะไม่ได้ใช้ lodash ใน index.js แต่ก็เป็นส่วนหนึ่งของผลลัพธ์ ซึ่งเพิ่มน้ำหนักให้กับเนื้อหาการผลิตของเราเป็นอย่างมาก

ต่อไปเราจะเปลี่ยนรูปแบบโมดูลเป็นโมดูล ECMAScript แล้วลองอีกครั้ง ในครั้งนี้ utils.js จะมีลักษณะดังนี้

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 จะนำเข้าจาก utils.js โดยใช้ไวยากรณ์โมดูล ECMAScript ดังนี้

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

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

เราสร้างแอปพลิเคชันและเปิดไฟล์เอาต์พุตได้โดยใช้การกำหนดค่า webpack เดียวกัน ขณะนี้มีขนาด 40 ไบต์ที่มีเอาต์พุตต่อไปนี้

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

โปรดทราบว่า Bundle สุดท้ายไม่มีฟังก์ชันจาก utils.js ที่เราไม่ได้ใช้ และไม่มีการติดตามจาก lodash นอกจากนี้ terser (ตัวลดขนาด JavaScript ที่ webpack ใช้) ได้แทรกฟังก์ชัน add ใน console.log ด้วย

คำถามที่คุณควรถามก็คือ ทำไมการใช้ CommonJS ทำให้กลุ่มเอาต์พุตใหญ่ขึ้นเกือบ 16,000 เท่า แน่นอนว่านี่เป็นตัวอย่างของเล่น ในความเป็นจริงแล้วความแตกต่างด้านขนาดอาจไม่ได้ใหญ่ขนาดนั้น แต่ก็มีโอกาสที่ CommonJS จะเพิ่มน้ำหนักให้กับงานสร้างของคุณได้มาก

ในกรณีทั่วไป โมดูล CommonJS จะเพิ่มประสิทธิภาพได้ยากกว่า เพราะมีการปรับเปลี่ยนแบบไดนามิกได้มากกว่าโมดูล ES โปรดหลีกเลี่ยงการใช้งานโมดูล CommonJS และใช้ไวยากรณ์โมดูล ECMAScript กับแอปพลิเคชันทั้งหมด เพื่อให้ Bundler และ Miniifier เพิ่มประสิทธิภาพแอปพลิเคชันได้สำเร็จ

โปรดทราบว่าแม้ว่าคุณจะใช้โมดูล ECMAScript ใน index.js อยู่ แต่หากโมดูลที่กําลังใช้เป็นโมดูล CommonJS ขนาด Bundle ของแอปจะได้รับผลกระทบ

เหตุใด CommonJS จึงทําให้แอปของคุณใหญ่ขึ้น

ในการตอบคำถามนี้ เราจะดูที่พฤติกรรมของ ModuleConcatenationPlugin ใน webpack และหลังจากนั้นให้พูดถึงความสามารถในการวิเคราะห์แบบคงที่ ปลั๊กอินนี้จะเชื่อมต่อขอบเขตของโมดูลทั้งหมดไว้ในที่เดียว และทำให้โค้ดของคุณใช้เวลาดำเนินการในเบราว์เซอร์ได้เร็วขึ้น ยกตัวอย่างเช่น:

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

ด้านบน เรามีโมดูล ECMAScript ซึ่งนำเข้าใน index.js เรายังกำหนดฟังก์ชัน subtract อีกด้วย เราอาจสร้างโปรเจ็กต์โดยใช้การกําหนดค่า webpack เดียวกันกับด้านบน แต่ครั้งนี้เราจะปิดใช้การลดขนาด:

const path = require('path');

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

มาดูผลลัพธ์ที่ได้กันดีกว่า

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

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

ในเอาต์พุตด้านบน ฟังก์ชันทั้งหมดอยู่ภายในเนมสเปซเดียวกัน Webpack ได้เปลี่ยนชื่อฟังก์ชัน subtract ใน index.js เป็น index_subtract เพื่อป้องกันการชนกัน

หากตัวลดขนาดประมวลผลซอร์สโค้ดด้านบน จะมีผลดังนี้

  • นำฟังก์ชัน subtract และ index_subtract ที่ไม่ได้ใช้ออก
  • นำความคิดเห็นและช่องว่างที่ซ้ำซ้อนออกทั้งหมด
  • แทรกในบรรทัดเนื้อหาของฟังก์ชัน add ในการเรียก console.log

นักพัฒนาซอฟต์แวร์มักมองว่าการนำการนำเข้าที่ไม่ได้ใช้ออกนั้นเป็นการเขย่าต้นไม้ การทำให้ต้นไม้สั่นสะเทือนนั้นเกิดขึ้นได้เนื่องจาก WebP สามารถทำความเข้าใจได้อย่างคงที่ (ณ เวลาที่สร้าง) ว่าเรากำลังนำเข้าสัญลักษณ์ใดจาก utils.js และส่งออกสัญลักษณ์ใด

ระบบจะเปิดใช้ลักษณะการทํางานนี้โดยค่าเริ่มต้นสําหรับโมดูล ES เนื่องจากสามารถวิเคราะห์ได้ในเชิงสถิติมากกว่า เมื่อเทียบกับ CommonJS

เราจะมาดูตัวอย่างเดิม แต่จะเปลี่ยน utils.js ไปใช้ CommonJS แทนโมดูล 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]);

การอัปเดตเล็กน้อยนี้จะเปลี่ยนแปลงผลลัพธ์ไปอย่างมาก เนื่องจากหน้าเว็บยาวเกินกว่าจะฝัง จึงขอแชร์เนื้อหาเพียงเล็กน้อย:

...
(() => {

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

})();

โปรดทราบว่า Bundle สุดท้ายมี "รันไทม์" ของ webpack บางรายการ: โค้ดที่แทรกไว้ซึ่งมีหน้าที่ในการนำเข้า/ส่งออกฟังก์ชันจากโมดูลที่รวมอยู่ในชุด ในครั้งนี้ แทนที่จะวางสัญลักษณ์ทั้งหมดจาก utils.js และ index.js ไว้ในเนมสเปซเดียวกัน เราจำเป็นต้องใช้ฟังก์ชัน add แบบไดนามิกขณะรันไทม์ที่ใช้ __webpack_require__

ซึ่งเป็นสิ่งจำเป็นเนื่องจากเราใช้ CommonJS เพื่อรับชื่อการส่งออกจากนิพจน์ที่กําหนดเองได้ ตัวอย่างเช่น โค้ดด้านล่างเป็นโครงสร้างที่ถูกต้องอย่างแน่นอน:

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

ทั้งนี้ ไม่มีทางที่ Bundler จะทราบ ณ เวลาบิลด์ว่าชื่อของสัญลักษณ์ที่ส่งออกคืออะไร เนื่องจากฟีเจอร์นี้ต้องใช้ข้อมูลที่ใช้ได้เฉพาะในรันไทม์ในบริบทของเบราว์เซอร์ของผู้ใช้

ด้วยวิธีนี้ ตัวลดขนาดจะไม่สามารถเข้าใจว่า index.js ใช้อะไรจากทรัพยากร Dependency แน่ๆ จึงไม่สามารถสกัดกั้นต้นไม้ได้ เราจะสังเกตเห็นลักษณะเดียวกันนี้สำหรับโมดูลของบุคคลที่สามเช่นกัน หากเรานำเข้าโมดูล CommonJS จาก node_modules เชนเครื่องมือบิลด์ของคุณจะเพิ่มประสิทธิภาพอย่างเหมาะสมไม่ได้

นั่งส่ายสะโพกไปกับ CommonJS

การวิเคราะห์โมดูล CommonJS นั้นทำได้ยากขึ้นเนื่องจากมีการเปลี่ยนแปลงแบบไดนามิกตามคำจำกัดความ ตัวอย่างเช่น ตำแหน่งการนำเข้าในโมดูล ES จะเป็นสตริงลิเทอรัลเสมอ เมื่อเทียบกับ CommonJS ซึ่งจะเป็นนิพจน์

ในบางกรณี หากไลบรารีที่คุณใช้อยู่เป็นไปตามหลักเกณฑ์เฉพาะในการใช้ CommonJS คุณสามารถนำการส่งออกที่ไม่ได้ใช้ออกในเวลาบิลด์ได้โดยใช้plugin webpack ของบุคคลที่สาม แม้ว่าปลั๊กอินนี้จะเพิ่มการรองรับการสั่นแบบต้นไม้ แต่ก็ไม่ได้ครอบคลุมวิธีต่างๆ ทั้งหมดที่ทรัพยากร Dependency สามารถใช้ CommonJS ได้ ซึ่งหมายความว่าคุณจะไม่ได้รับการรับประกันเช่นเดียวกันกับโมดูล ES นอกจากนี้ยังมีค่าใช้จ่ายเพิ่มเติมในขั้นตอนการสร้างนอกเหนือจากลักษณะการทำงาน webpack เริ่มต้น

บทสรุป

โปรดหลีกเลี่ยงการขึ้นอยู่กับโมดูล CommonJS และใช้ไวยากรณ์โมดูล ECMAScript กับแอปพลิเคชันทั้งหมด เพื่อให้มั่นใจว่า Bundler จะเพิ่มประสิทธิภาพแอปพลิเคชันได้สําเร็จ

ต่อไปนี้เป็นเคล็ดลับบางส่วนที่นำไปปฏิบัติได้เพื่อยืนยันว่าคุณมาถูกทางแล้ว

  • ใช้ปลั๊กอิน node-resolve ของ Rollup.js และตั้งค่าแฟล็ก modulesOnly เพื่อระบุว่าคุณต้องการใช้โมดูล ECMAScript เท่านั้น
  • ใช้แพ็กเกจ is-esm เพื่อยืนยันว่าแพ็กเกจ npm ใช้โมดูล ECMAScript
  • หากกำลังใช้ Angular คุณจะได้รับคำเตือนโดยค่าเริ่มต้นหากใช้โมดูลที่เคลื่อนย้ายไม่ได้