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