เว็บแอปพลิเคชันในปัจจุบันนี้ค่อนข้างใหญ่มาก โดยเฉพาะส่วนที่เป็น JavaScript ตั้งแต่กลางปี 2018 ที่เก็บถาวรของ HTTP จะวางขนาดการโอนค่ามัธยฐานของ JavaScript บนอุปกรณ์เคลื่อนที่ที่ประมาณ 350 KB ขนาดนี้โอนเท่านั้น JavaScript มักจะได้รับการบีบอัดเมื่อส่งผ่านเครือข่าย ซึ่งหมายความว่าปริมาณตามจริงของ JavaScript จะค่อนข้างมากขึ้นหลังจากที่เบราว์เซอร์คลายการบีบอัด สิ่งสำคัญที่ต้องเน้นย้ำก็คือ การบีบอัดไม่มีความเกี่ยวข้องเท่าที่ควรจะเป็นเพราะการประมวลผลทรัพยากร 900 KB ของ JavaScript ที่บีบอัดยังคงเป็น 900 KB สำหรับโปรแกรมแยกวิเคราะห์และคอมไพเลอร์ แม้ว่าอาจมีขนาดประมาณ 300 KB เมื่อบีบอัด
JavaScript เป็นทรัพยากรที่มีราคาแพงในการประมวลผล JavaScript จะต้องได้รับการแยกวิเคราะห์ คอมไพล์ แล้วดำเนินการในที่สุด ซึ่งแตกต่างจากรูปภาพที่ใช้เวลาถอดรหัสเพียงเล็กน้อยเมื่อดาวน์โหลด ไบต์ต่อไบต์ซึ่งทำให้ JavaScript มีราคาแพงกว่าทรัพยากรประเภทอื่นๆ
ในขณะที่มีการปรับปรุงอย่างต่อเนื่องเพื่อปรับปรุงประสิทธิภาพของเครื่องมือ JavaScript แต่การปรับปรุงประสิทธิภาพของ JavaScript ก็ยังคงเป็นหน้าที่สำหรับนักพัฒนาซอฟต์แวร์เสมอ
ด้วยเหตุนี้เราจึงมีเทคนิคในการปรับปรุงประสิทธิภาพ JavaScript การแยกโค้ดเป็นเทคนิคหนึ่งที่ช่วยปรับปรุงประสิทธิภาพด้วยการแบ่งพาร์ติชัน JavaScript ของแอปพลิเคชันออกเป็นส่วนๆ และแสดงชิ้นส่วนเหล่านั้นไปยังเส้นทางของแอปพลิเคชันที่จำเป็นต้องใช้เท่านั้น
แม้ว่าเทคนิคนี้จะใช้ได้ผล แต่ก็ไม่สามารถแก้ปัญหาโดยทั่วไปของแอปพลิเคชันที่ใช้ JavaScript จำนวนมากได้ ซึ่งเป็นการรวมโค้ดที่ไม่เคยใช้งานมาก่อน การสั่นสะเทือนของต้นไม้พยายามแก้ไขปัญหานี้
ต้นไม้สั่นสะเทือนคืออะไร
การสั่นสะเทือนของต้นไม้เป็นรูปแบบหนึ่งของการกำจัดโค้ดที่ตายแล้ว คำนี้เป็นที่นิยมใน Rollup แต่แนวคิดการกำจัดโค้ดที่ใช้งานไม่ได้นั้นมีอยู่มาสักระยะหนึ่งแล้ว แนวคิดยังพบการซื้อใน Webpack ด้วย ซึ่งดูได้จากตัวอย่างแอปในบทความนี้
คำว่า "การสั่นสะเทือนของต้นไม้" มาจากโมเดลทางความคิดของแอปพลิเคชันของคุณและการขึ้นต่อกันของแอปพลิเคชันนี้เป็นโครงสร้างที่เหมือนต้นไม้ แต่ละโหนดในโครงสร้างแสดงถึงทรัพยากร Dependency ที่ให้ฟังก์ชันการทำงานที่แตกต่างกันสําหรับแอป ในแอปสมัยใหม่ ทรัพยากร Dependency ต่อไปนี้จะนำมาผ่านคำสั่ง import
แบบคงที่ ดังนี้
// Import all the array utilities!
import arrayUtils from "array-utils";
เมื่อแอปยังอยู่ในช่วงวัยรุ่น แอปก็อาจต้องใช้ทรัพยากร Dependency เพียงเล็กน้อย (หากทำได้) นอกจากนี้ยังใช้ทรัพยากร Dependency ส่วนใหญ่ (หรือไม่ใช่ทั้งหมด) ที่คุณเพิ่มด้วย แต่จะเพิ่มทรัพยากร Dependency ได้มากขึ้นเมื่อแอปเติบโต ในการประกอบกรณี ทรัพยากร Dependency เก่าจะใช้งานไม่ได้ แต่อาจไม่ถูกตัดออกจากฐานของโค้ด สุดท้ายแล้ว แอปจะมี JavaScript ที่ไม่ได้ใช้เป็นจำนวนมาก การสั่นสะเทือนจากต้นไม้ช่วยแก้ปัญหานี้โดยใช้ประโยชน์จากการดึงคำสั่ง import
แบบคงที่ในส่วนที่เฉพาะเจาะจงของโมดูล ES6 ดังนี้
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
ความแตกต่างระหว่างตัวอย่าง import
นี้กับตัวอย่างก่อนหน้าคือ แทนที่จะนำเข้าทุกอย่างจากโมดูล "array-utils"
ซึ่งอาจเป็นโค้ดจำนวนมาก) ตัวอย่างนี้จะนำเข้าเฉพาะบางส่วนของตัวอย่างเท่านั้น ในบิลด์ของนักพัฒนาซอฟต์แวร์ ส่วนนี้จะไม่เปลี่ยนแปลงอะไร เนื่องจากโมดูลทั้งหมดจะมีการนำเข้าไม่ว่าในกรณีใดก็ตาม ในบิลด์ที่ใช้งานจริง คุณสามารถกำหนดค่า Webpack ให้ "เขย่า" ได้ ปิด export จากโมดูล ES6 ที่ไม่ได้นำเข้าอย่างชัดเจน ส่งผลให้เวอร์ชันที่ใช้งานจริงเหล่านั้นมีขนาดเล็กลง ในคู่มือนี้ คุณจะได้ทราบวิธีทำแบบนั้น
หาโอกาสเขย่าต้นไม้
เพื่อวัตถุประสงค์ในการอธิบายให้เห็นภาพ เรามีตัวอย่างแอปขนาด 1 หน้าที่สาธิตวิธีการทำงานของการสั่นสะเทือนของต้นไม้ คุณสามารถโคลนและทำตามได้ถ้าต้องการ แต่เราจะอธิบายทุกขั้นตอนไปด้วยกันในคู่มือนี้ คุณจึงไม่จำเป็นต้องทำการโคลน (เว้นแต่คุณจะต้องเรียนรู้ด้วยตนเอง)
แอปตัวอย่างนี้เป็นฐานข้อมูลที่ค้นหาได้ของแป้นเหยียบเอฟเฟกต์กีตาร์ คุณป้อนคำค้นหาและรายการแป้นเอฟเฟกต์จะปรากฏขึ้น
ลักษณะการทำงานที่ขับเคลื่อนแอปนี้จะแยกออกจากผู้ให้บริการ (เช่น Preact และ Emotion) และ Code Bundle สําหรับแอปเฉพาะ (หรือ "กลุ่ม" ตามที่ Webpack เรียกใช้)
แพ็กเกจ JavaScript ที่แสดงในรูปด้านบนเป็นบิลด์ในเวอร์ชันที่ใช้งานจริง ซึ่งหมายความว่าจะได้รับการเพิ่มประสิทธิภาพผ่านการรวมภายนอก 21.1 KB สำหรับ Bundle เฉพาะแอปไม่ถือว่าแย่ แต่ควรทราบว่าไม่มีการสั่นสะเทือนของต้นไม้ไม่ว่าในกรณีใดๆ เรามาดูโค้ดของแอปกันเพื่อดูว่าจะทำอะไรได้บ้างเพื่อแก้ปัญหานี้
การใช้งานใดๆ ก็ตาม การหาโอกาสเกิดการสั่นสะเทือนของต้นไม้จะต้องมองหาข้อความ import
แบบคงที่ บริเวณด้านบนของไฟล์คอมโพเนนต์หลัก คุณจะเห็นบรรทัดดังนี้
import * as utils from "../../utils/utils";
คุณสามารถนำเข้าโมดูล ES6 ได้หลายวิธี แต่โมดูลแบบนี้ควรได้รับความสนใจจากคุณ บรรทัดนี้ระบุว่า "import
ทุกอย่างจากโมดูล utils
และใส่ไว้ในเนมสเปซที่ชื่อ utils
" คำถามสำคัญที่ควรถามตอนนี้คือ "มีเนื้อหามากน้อยแค่ไหนในโมดูลนี้"
หากดูที่ซอร์สโค้ดของโมดูล utils
คุณจะเห็นว่ามีโค้ดประมาณ 1,300 บรรทัด
คุณต้องการสิ่งเหล่านั้นไหม มาตรวจสอบอีกครั้งด้วยการค้นหาไฟล์คอมโพเนนต์หลักที่นำเข้าโมดูล utils
เพื่อดูจำนวนอินสแตนซ์ของเนมสเปซนั้น
ผลที่ได้คือเนมสเปซ utils
จะปรากฏในแอปพลิเคชันเพียง 3 จุดเท่านั้น แต่มีไว้เพื่อฟังก์ชันใด หากคุณดูไฟล์คอมโพเนนต์หลักอีกครั้ง จะเห็นเพียงฟังก์ชันเดียวซึ่งก็คือ utils.simpleSort
ซึ่งใช้เพื่อจัดเรียงรายการผลการค้นหาตามเกณฑ์ต่างๆ เมื่อมีการเปลี่ยนแปลงเมนูแบบเลื่อนลงของการจัดเรียง
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
จากไฟล์บรรทัด 1,300 ไฟล์ที่มีการส่งออกจำนวนมาก จะใช้เพียง 1 รายการเท่านั้น ซึ่งส่งผลให้มีการจัดส่ง JavaScript จำนวนมากที่ไม่ได้ใช้
แม้ว่าแอปตัวอย่างนี้จะมีแนวคิดอยู่บ้าง แต่ก็ไม่ได้เปลี่ยนแปลงข้อเท็จจริงที่ว่าสถานการณ์สังเคราะห์นี้คล้ายคลึงกับโอกาสในการเพิ่มประสิทธิภาพจริงที่คุณอาจพบในเว็บแอปเวอร์ชันที่ใช้งานจริง เมื่อคุณได้ระบุโอกาสในการเกิดการสั่นสะเทือนของต้นไม้ให้เป็นประโยชน์ได้แล้ว จริงๆ แล้วมันเกิดขึ้นได้อย่างไร
ป้องกันไม่ให้ Babel แปลงโมดูล ES6 ไปยังโมดูล CommonJS
Babel เป็นเครื่องมือที่ขาดไม่ได้ แต่อาจทำให้สังเกตผลกระทบจากการสั่นสะเทือนของต้นไม้ได้ยากขึ้นเล็กน้อย หากคุณใช้ @babel/preset-env
อยู่ Babel อาจเปลี่ยนโมดูล ES6 เป็นโมดูล CommonJS ที่เข้ากันได้มากขึ้น กล่าวคือ โมดูลที่คุณ require
แทน import
เนื่องจากการสั่นสะเทือนของต้นไม้นั้นทำได้ยากกว่าสำหรับโมดูล CommonJS ดังนั้น Webpack จึงไม่ทราบว่าต้องตัดออกจากชุดซอฟต์แวร์อย่างไรหากคุณตัดสินใจที่จะใช้ วิธีแก้ไขคือให้กำหนดค่า @babel/preset-env
เพื่อเว้นโมดูล ES6 ไว้อย่างชัดแจ้ง ไม่ว่าคุณจะกำหนดค่า Babel ไว้ที่ใด ไม่ว่าจะเป็นใน babel.config.js
หรือ package.json
จะต้องมีการเพิ่มองค์ประกอบเพิ่มเติมเล็กๆ น้อยๆ ดังนี้
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
การระบุ modules: false
ในการกำหนดค่า @babel/preset-env
จะทำให้ Babel ทำงานตามที่ต้องการ ซึ่งทำให้ Webpack วิเคราะห์โครงสร้างทรัพยากร Dependency และสลัดทรัพยากร Dependency ที่ไม่ได้ใช้ได้
คำนึงถึงผลข้างเคียง
อีกแง่มุมหนึ่งที่ควรพิจารณาเมื่อเขย่า Dependency จากแอปคือ โมดูลของโครงการจะมีผลข้างเคียง ตัวอย่างของผลข้างเคียงคือ จะแก้ไขบางอย่างนอกขอบเขตของตัวเองซึ่งเป็นผลข้างเคียง ของการดำเนินการ
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
ในตัวอย่างนี้ addFruit
จะสร้างผลข้างเคียงเมื่อแก้ไขอาร์เรย์ fruits
ซึ่งอยู่นอกขอบเขต
ผลข้างเคียงก็ใช้ได้กับโมดูล ES6 และมีความสำคัญในบริบทของการสั่นสะเทือนของต้นไม้ โมดูลที่ใช้อินพุตที่คาดการณ์ได้และสร้างเอาต์พุตที่คาดการณ์ได้อย่างเท่าเทียมกันโดยไม่แก้ไขสิ่งอื่นนอกขอบเขตของตนเองนั้น เป็นทรัพยากร Dependency ที่สามารถนำอินพุตดังกล่าวออกได้อย่างปลอดภัยหากเราไม่ได้ใช้ โค้ดดังกล่าวเป็นของโค้ดที่ประกอบขึ้นแล้วแบบแยกส่วน ดังนั้น "โมดูล"
ในกรณีที่มีข้อกังวลเกี่ยวกับ Webpack คุณสามารถใช้คำแนะนำเพื่อระบุว่าแพ็กเกจและทรัพยากร Dependency จะไม่ได้รับผลกระทบข้างเคียง โดยระบุ "sideEffects": false
ในไฟล์ package.json
ของโปรเจ็กต์ ดังนี้
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
นอกจากนี้ คุณยังบอก Webpack ได้ว่าไฟล์ใดไม่ได้รับผลกระทบจากผลข้างเคียง โดยทำดังนี้
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
ในตัวอย่างหลัง ไฟล์ใดๆ ที่ไม่ได้ระบุจะได้รับการสันนิษฐานว่าปราศจากผลข้างเคียง หากไม่ต้องการเพิ่มรายการนี้ลงในไฟล์ package.json
คุณสามารถระบุ Flag นี้ในการกำหนดค่า Webpack ผ่าน module.rules
ได้ด้วย
นำเข้าเฉพาะสิ่งที่จำเป็น
หลังจากที่สั่งให้ Babel ปล่อยโมดูล ES6 ไว้ตามลำพังแล้ว ก็จำเป็นต้องมีการปรับเปลี่ยนไวยากรณ์ import
เล็กน้อยเพื่อนำเฉพาะฟังก์ชันที่จำเป็นจากโมดูล utils
เข้ามา ในตัวอย่างของคู่มือนี้ ทุกสิ่งที่ต้องใช้คือฟังก์ชัน simpleSort
import { simpleSort } from "../../utils/utils";
เนื่องจากระบบจะนำเข้าเฉพาะ simpleSort
เท่านั้น ไม่ใช่ทั้งโมดูล utils
ทุกอินสแตนซ์ของ utils.simpleSort
จึงต้องเปลี่ยนเป็น simpleSort
:
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
ในตัวอย่างนี้ควรเป็นสิ่งจำเป็นสำหรับการสั่นสะเทือนของต้นไม้ นี่คือเอาต์พุตของ Webpack ก่อนที่จะเขย่าแผนผัง Dependency
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
นี่คือเอาต์พุตหลังจากที่ต้นไม้สั่นสะเทือนสำเร็จแล้ว
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
แม้ว่าแพ็กเกจทั้ง 2 แพ็กเกจจะหดตัว แต่แท้จริงแล้วแพ็กเกจ main
ที่ได้ประโยชน์มากที่สุด การเขย่าส่วนที่ไม่ได้ใช้ของโมดูล utils
ทำให้แพ็กเกจ main
เล็กลงประมาณ 60% ซึ่งไม่เพียงจะลดระยะเวลาที่ใช้ในการดาวน์โหลดสคริปต์ แต่ยังลดเวลาในการประมวลผลอีกด้วย
ไปเขย่าต้นไม้กัน!
ระยะทางใดก็ตามที่คุณได้รับจากการสั่นสะเทือนของต้นไม้จะขึ้นอยู่กับแอปของคุณ รวมถึงทรัพยากร Dependency และสถาปัตยกรรมของแอป ลองใช้งาน หากคุณทราบข้อเท็จจริงว่าคุณยังไม่ได้ตั้งค่า Bundler โมดูลเพื่อเพิ่มประสิทธิภาพนี้ ก็ไม่เสียหายที่จะลองใช้และดูว่าคุณจะได้รับประโยชน์อะไรจากแอปพลิเคชันของคุณ
คุณอาจตระหนักถึงประสิทธิภาพที่เพิ่มขึ้นอย่างมากจากการสั่นสะเทือนของต้นไม้ หรือไม่ได้เพิ่มขึ้นมากนัก แต่การกำหนดค่าระบบบิลด์เพื่อใช้ประโยชน์จากการเพิ่มประสิทธิภาพนี้ในเวอร์ชันที่ใช้งานจริงและเลือกนำเข้าเฉพาะสิ่งที่แอปพลิเคชันต้องการเท่านั้น คุณจะทำให้แพ็กเกจแอปพลิเคชันมีขนาดเล็กที่สุดในเชิงรุก
ขอขอบคุณ Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone และ Philip Walton สำหรับความคิดเห็นที่มีค่า ซึ่งช่วยปรับปรุงคุณภาพของบทความนี้ได้อย่างมาก