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

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

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

สรุป: หลีกเลี่ยงการพึ่งพาโมดูล 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 จึงไม่ได้ออกแบบมาเพื่อลดขนาดของ Bundle เวอร์ชันที่ใช้งานจริง ในขณะเดียวกัน การวิเคราะห์แสดงให้เห็นว่าขนาดกลุ่ม JavaScript ยังคงเป็นสาเหตุอันดับหนึ่งที่ทําให้แอปเบราว์เซอร์ช้าลง

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 แล้ว หากสำรวจขนาด output เราจะเห็นข้อมูลประมาณนี้

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

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

ตอนนี้เรามาเปลี่ยนรูปแบบโมดูลเป็นโมดูล 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 และหลังจากนั้นจึงจะพูดถึงความสามารถในการวิเคราะห์แบบคงที่ ปลั๊กอินนี้จะต่อขอบเขตของโมดูลทั้งหมดเข้าด้วยกันเป็น Closure รายการเดียว และช่วยให้โค้ดของคุณมีเวลาในการดำเนินการเร็วขึ้นในเบราว์เซอร์ ยกตัวอย่างเช่น:

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

บ่อยครั้งที่นักพัฒนาซอฟต์แวร์เรียกการนำการนําเข้าที่ไม่ได้ใช้ออกว่า "Tree-shaking" การเขย่าต้นไม้เกิดขึ้นได้เพราะ 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())] = () => {  };

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

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

Tree-shaking ด้วย CommonJS

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

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

บทสรุป

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

เคล็ดลับที่นําไปใช้ได้จริง 2-3 ข้อต่อไปนี้จะช่วยให้คุณมั่นใจว่าคุณกำลังเดินอยู่บนเส้นทางที่ดีที่สุด

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