เว็บแอปพลิเคชันทุกวันนี้อาจมีจำนวนมหาศาล โดยเฉพาะส่วนที่เป็น JavaScript ตั้งแต่ช่วงกลางปี 2018 ที่เก็บถาวร HTTP จะทำให้ขนาดการโอนของ JavaScript ในอุปกรณ์เคลื่อนที่ตามค่ามัธยฐานอยู่ที่ประมาณ 350 KB และนี่เป็นเพียงขนาดการโอนเท่านั้น JavaScript มักถูกบีบอัดเมื่อส่งผ่านเครือข่าย ซึ่งหมายความว่าจำนวน JavaScript จริงจะมากกว่านี้เล็กน้อยหลังจากที่เบราว์เซอร์ขยายการบีบอัด โปรดทราบว่าสำหรับการประมวลผลทรัพยากรแล้ว การบีบอัดนั้นไม่มีความเกี่ยวข้องแต่อย่างใด JavaScript ที่บีบอัดขนาด 900 KB จะยังคงเป็น 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 เก่าจะใช้งานไม่ได้ แต่อาจไม่ได้รับการตัดออกจากฐานของโค้ด ผลลัพธ์ที่ได้คือแอปก็ส่งสินค้าโดยมี JavaScript ที่ไม่ได้ใช้เป็นจำนวนมาก การสั่นสะเทือนของต้นไม้ช่วยแก้ปัญหานี้ได้โดยใช้การใช้ประโยชน์จากคำสั่ง import
แบบคงที่ในการดึงส่วนที่เฉพาะเจาะจงของโมดูล ES6 ดังนี้
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
ความแตกต่างระหว่างตัวอย่าง import
นี้กับตัวอย่างก่อนหน้าคือ แทนที่จะนำเข้าทุกอย่างจากโมดูล "array-utils"
ซึ่งอาจเป็นโค้ดจำนวนมาก) ตัวอย่างนี้จะนำเข้าเฉพาะบางส่วนของโค้ด ในรุ่นนักพัฒนาซอฟต์แวร์ ขั้นตอนนี้จะไม่เปลี่ยนแปลงอะไร เนื่องจากจะมีการนำเข้าโมดูลทั้งโมดูล ในบิลด์ที่ใช้งานจริง คุณสามารถกำหนดค่า Webpack ให้ "ส่าย" การส่งออกจากโมดูล ES6 ที่ไม่ได้นำเข้าอย่างชัดเจนได้ ซึ่งทำให้บิลด์ที่ใช้งานจริงเหล่านั้นมีขนาดเล็กลง ในคู่มือนี้ คุณจะได้ทราบวิธีทำอย่างนั้น
มองหาโอกาสในการเขย่าต้นไม้
เพื่อเป็นตัวอย่างประกอบ เรามีตัวอย่างแอปแบบ 1 หน้าที่สาธิตวิธีการทำงานของการเขย่าต้นไม้ คุณสามารถโคลนและทำตามได้ถ้าต้องการ แต่เราจะอธิบายทุกขั้นตอนในคู่มือนี้ การโคลนจึงไม่จำเป็น (เว้นแต่ว่าคุณจะเรียนรู้แบบลงมือทำเอง)
แอปตัวอย่างเป็นฐานข้อมูลที่ค้นหาได้ของแป้นเหยียบเอฟเฟกต์กีตาร์ คุณป้อนคำค้นหา แล้วรายการแป้นเหยียบเอฟเฟกต์จะปรากฏขึ้น
ระบบจะแยกลักษณะการทำงานที่ทำให้เกิดแอปนี้ออกเป็นผู้ให้บริการ (เช่น Preact และ Emotion) รวมถึงชุดโค้ดเฉพาะแอป (หรือ "ส่วน" ตามที่ 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 บรรทัดที่มีการส่งออกจำนวนมากจะใช้เพียงไฟล์เดียวเท่านั้น ซึ่งส่งผลให้มีการจัดส่ง JavaScript ที่ไม่ได้ใช้จำนวนมาก
แม้ว่าแอปตัวอย่างนี้จะได้รับการสร้างขึ้นเพียงเล็กน้อย แต่ก็ไม่ได้เปลี่ยนแปลงข้อเท็จจริงที่ว่าสถานการณ์การสังเคราะห์นี้คล้ายกับโอกาสในการเพิ่มประสิทธิภาพที่เกิดขึ้นจริงที่คุณอาจพบในเว็บแอปที่ใช้งานจริง เมื่อคุณได้ระบุโอกาสที่จะเกิดการสั่นสะเทือนของต้นไม้ว่าจะเป็นประโยชน์แล้ว จริงๆ แล้วคุณดำเนินการอย่างไร
การป้องกัน Babel จากการเปลี่ยนรูปแบบโมดูล ES6 ไปยังโมดูล CommonJS
Babel เป็นเครื่องมือที่ขาดไม่ได้ แต่ก็อาจทำให้เห็นผลจากการสั่นสะเทือนของต้นไม้ได้ยากขึ้นเล็กน้อย หากคุณใช้ @babel/preset-env
Babel อาจแปลงโมดูล ES6 เป็นโมดูล CommonJS ที่เข้ากันได้อย่างกว้างขวางมากขึ้น ซึ่งก็คือโมดูลที่คุณ require
แทนที่จะเป็น import
เนื่องจากการสั่นสะเทือนของต้นไม้ทำได้ยากกว่าสำหรับโมดูล CommonJS ดังนั้น Webpack จึงจะไม่รู้ว่าควรตัด Bundle อย่างไรหากคุณตัดสินใจที่จะใช้ วิธีแก้ไขคือกำหนดค่า @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
คุณระบุแฟล็กนี้ในการกำหนดค่า 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
แม้ว่าแพ็กเกจทั้งสองจะหดตัวลง แต่ก็เป็นแพ็กเกจ main
ที่มีประโยชน์มากที่สุดจริงๆ การแยกส่วนที่ไม่ได้ใช้ของโมดูล utils
ออกจะทำให้แพ็กเกจ main
ลดลงประมาณ 60% วิธีนี้ไม่เพียงลดระยะเวลาที่สคริปต์ใช้ในการดาวน์โหลด แต่ยังลดเวลาในการประมวลผลด้วย
ไปส่ายต้นไม้กันเถอะ
ไม่ว่าคุณจะใช้ระยะทางมากน้อยเพียงใดจากการสั่นสะเทือนของต้นไม้ ก็ขึ้นอยู่กับแอป รวมถึงทรัพยากร Dependency และสถาปัตยกรรมของแอปด้วย ลองใช้เลย หากคุณทราบว่าคุณยังไม่ได้ตั้งค่า Module Bundler เพื่อทำการเพิ่มประสิทธิภาพนี้ ก็ไม่เสียหายที่จะลองและดูว่าได้รับประโยชน์อย่างไรจากแอปพลิเคชันของคุณ
คุณอาจตระหนักถึงประสิทธิภาพที่เพิ่มขึ้นอย่างมากจากการสั่นสะเทือนของต้นไม้ หรือไม่ส่งผลใดๆ เลย แต่การกำหนดค่าระบบบิลด์เพื่อใช้ประโยชน์จากการเพิ่มประสิทธิภาพนี้ในบิลด์เวอร์ชันที่ใช้งานจริงและเลือกนำเข้าเฉพาะสิ่งที่แอปพลิเคชันต้องการเท่านั้นจะทำให้แพ็กเกจแอปพลิเคชันมีขนาดเล็กที่สุดในเชิงรุกได้
ขอขอบคุณ Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone และ Philip Walton สำหรับความคิดเห็นที่มีค่า ซึ่งช่วยปรับปรุงคุณภาพของบทความนี้ได้อย่างมาก