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

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

มินโก เกเชฟ
Minko Gechev

ในโพสต์นี้ เราจะมาดูกันว่า 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 คุณจะได้รับคำเตือนโดยค่าเริ่มต้นหากใช้โมดูลที่เคลื่อนย้ายไม่ได้