วิธีที่ 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 ยังคงเป็นสาเหตุอันดับหนึ่งที่ทําให้แอปเบราว์เซอร์ช้าลง

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

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

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

บทสรุป

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

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

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