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