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

ดูว่าโมดูล CommonJS ส่งผลต่อการสั่นสะเทือนของแอปพลิเคชันอย่างไร

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

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

CommonJS คืออะไร

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

เมื่อใช้ 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 ที่ทำให้แอปเบราว์เซอร์ทำงานช้าลง

Bundler และเครื่องมือลดขนาด 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)})();

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

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

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

โปรดทราบว่าแม้จะใช้โมดูล ECMAScript ใน index.js แต่หากโมดูลที่ใช้เป็นโมดูล CommonJS ขนาดแพ็กเกจของแอปจะลดลง

ทำไม 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

นักพัฒนาซอฟต์แวร์มักจะเรียกกันว่าการนำการนำเข้าที่ไม่ได้ใช้ออกเป็นการเขย่าต้นไม้ การเขย่าต้นไม้เกิดขึ้นได้เพราะ Webpack สามารถทำความเข้าใจแบบคงที่ (ณ เวลาสร้าง) ว่าเรานำเข้าสัญลักษณ์ใดจาก 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));

})();

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

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

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

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

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

การเขย่าต้นไม้กับ CommonJS

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

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

บทสรุป

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

เคล็ดลับที่นำไปใช้ได้จริงเพื่อยืนยันว่าคุณอยู่ในเส้นทางที่เหมาะสมมีดังนี้

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