Tree Shaking — กลไกการกำจัดโค้ดที่ไม่ใช้แล้ว (dead code elimination) ในขั้นตอนการสร้างแอปพลิเคชัน Tree Shaking วิเคราะห์โครงสร้างแบบคงที่ของโมดูล ES และแยกฟังก์ชัน คลาส และตัวแปรที่ถูกส่งออก (export) ซึ่งไม่ได้ถูกนำเข้า (import) ไว้ที่ใด ตาม Webpack Documentation การกำหนดค่า Tree Shaking ที่ถูกต้องสามารถลดขนาด bundle ได้ 30–60% โดยไม่เปลี่ยนฟังก์ชันการทำงานของแอปพลิเคชัน
ประเด็นสำคัญ
Tree Shaking — เทคนิคการเพิ่มประสิทธิภาพโค้ดที่แยกโมดูลและฟังก์ชันที่ไม่ได้ใช้ออกจาก bundle สุดท้าย คำศัพท์นี้ถูกนำเสนอโดยทีม Rollup ในปี 2015 และอธิบายกระบวนการในเชิงอุปมาอุปไมย: ต้นไม้การพึ่งพาถูกเขย่า (shake) และกิ่งที่ไม่ได้ใช้จะหลุดร่วง แตกต่างจากการเพิ่มประสิทธิภาพด้วยตนเอง Tree Shaking จะทำงานโดยอัตโนมัติในขั้นตอนการสร้าง
Tree Shaking ทำงานเฉพาะกับ โมดูล ES (ECMAScript Modules) ซึ่งการพึ่งพาจะถูกกำหนดแบบคงที่ผ่าน import และ export CommonJS (require/module.exports) ไม่รองรับ Tree Shaking เพราะ require ถูกดำเนินการแบบไดนามิก — ตัวรวมบัณฑลไม่สามารถระบุล่วงหน้าได้ว่าฟังก์ชันใดถูกใช้จริง ไลบรารีสมัยใหม่ (Lodash, Moment.js, RxJS) เผยแพร่เวอร์ชัน ES เพื่อรองรับ Tree Shaking
Rollup — ตัวรวมบัณฑลแรกที่นำ Tree Shaking มาใช้ในปี 2015 แตกต่างจาก Webpack ตรงที่ Rollup ถูกออกแบบมาสำหรับโมดูล ES ตั้งแต่แรกเริ่มและดำเนินการกำจัดโค้ดที่ตายแล้วอย่างจริงจังมากขึ้น Rollup วิเคราะห์ไม่เพียงแค่ export แต่ละรายการ แต่รวมถึงทั้งโมดูล: ถ้าโมดูลไม่มีผลข้างเคียงและไม่มี export ใดถูกใช้ Rollup จะแยกทั้งโมดูลออกจาก bundle
Rollup มีประสิทธิภาพโดยเฉพาะสำหรับไลบรารีและ SDK ที่ทุกกิโลไบต์มีความสำคัญ เฟรมเวิร์ก Vue.js ใช้ Rollup เพื่อสร้างเวอร์ชัน production React ย้ายไปใช้ Rollup ในปี 2020 สำหรับแอปพลิเคชัน Webpack ถูกใช้บ่อยกว่าเนื่องจากระบบนิเวศปลั๊กอินที่สมบูรณ์กว่า (Hot Module Replacement, code splitting, CSS modules) แต่สำหรับ Tree Shaking สูงสุดเมื่อสร้างไลบรารี Rollup ยังคงเป็นมาตรฐานอุตสาหกรรม
การประหยัด จาก Tree Shaking ขึ้นอยู่กับสถาปัตยกรรมของโปรเจกต์อย่างมาก ในแอปพลิเคชัน React ที่ใช้ไลบรารี Ant Design Tree Shaking สามารถลบโค้ดส่วนประกอบ UI ได้ถึง 70% ในโปรเจกต์ที่ import ทั้งหมดมีความเฉพาะเจาะจงและตรงเป้า การประหยัดจะอยู่ที่ 5–15% ตามการวิจัยของ Webpack การประหยัดโดยเฉลี่ยคือ 30–40% ของขนาด bundle
| ประเภทโค้ดที่ตายแล้ว | ตัวอย่าง | การตรวจจับ Tree Shaking |
|---|---|---|
| Export ที่ไม่ได้ใช้ | export function unusedHelper() | ใช่ |
| Import ที่ไม่ได้ใช้ | import { unused } from "lib" | ใช่ |
| กิ่งเงื่อนไขที่ตายแล้ว | if (false) { ... } | ไม่ (ถูกลบโดย minifier) |
| ฟังก์ชันที่ไม่ถูกเรียกหลังจาก DCE | function a(){} a() โดยที่ a ไม่ถูกเรียก | บางส่วน |
กลไก Tree Shaking ขึ้นอยู่กับกราฟการพึ่งพาที่ตัวรวมบัณฑลสร้างจากคำสั่ง import/export ทั้งหมดในโปรเจกต์ ในขั้นแรก ตัวรวมบัณฑลจะสำรวจไฟล์ทั้งหมดจากจุดเริ่มต้น (entry point) และรวบรวมต้นไม้โมดูล ในขั้นที่สอง จะวิเคราะห์ว่า export ใดจากแต่ละโมดูลถูกนำเข้าในโมดูลอื่นจริง ๆ
สำหรับแต่ละโมดูล Webpack หรือ Rollup จะทำเครื่องหมาย export ว่าใช้แล้วหรือไม่ได้ใช้ export ที่ไม่ได้ใช้จะถูกแยกออกจาก bundle อย่างไรก็ตาม โมดูลเองยังคงอยู่ใน bundle ถ้ามี export อย่างน้อยหนึ่งรายการถูกใช้ โมดูลสามารถถูกแยกออกทั้งหมดได้ผ่านแฟล็ก sideEffects เท่านั้น หรือถ้าโมดูลไม่มีผลข้างเคียงใด ๆ
// utils.js — โมดูลที่มีฟังก์ชัน
export function formatDate(date) {
return date.toISOString().slice(0, 10);
}
export function formatCurrency(amount) {
return "$" + amount.toFixed(2);
}
export function slugify(text) {
return text.toLowerCase().replace(/\s+/g, "-");
}// app.js — จุดเริ่มต้น
import { formatDate } from "./utils";
const today = formatDate(new Date());
console.log(today);// หลัง Tree Shaking — ใน bundle มีเฉพาะ formatDate
function formatDate(date) {
return date.toISOString().slice(0, 10);
}
const today = formatDate(new Date());
console.log(today);Tree Shaking ได้แยก formatCurrency และ slugify ออกจาก bundle สุดท้ายเพราะไม่ได้ถูก import ใน app.js ขนาดของโมดูล utils.js ลดลงจาก 3 ฟังก์ชันเหลือ 1 ถ้า utils.js มีผลข้างเคียง (เช่น การเริ่มต้นทั่วโลก) Tree Shaking จะไม่สามารถลบแม้แต่ export ที่ไม่ได้ใช้
Webpack รวมการรองรับ Tree Shaking ในตัวผ่าน TerserPlugin ในโหมด production การเปิดใช้งาน Tree Shaking ใช้เพียงสองเงื่อนไข: mode ถูกตั้งค่าเป็น production (mode: "production") และโมดูลใช้ไวยากรณ์ ES (import/export) Webpack จะทำเครื่องหมาย export ที่ไม่ได้ใช้โดยอัตโนมัติและส่งต่อไปยัง Terser เพื่อลบ
การกำหนดค่าเพิ่มเติม usedExports: true ใน optimization.webpack.config.js ช่วยให้วิเคราะห์การใช้ export ภายในโมดูลอย่างละเอียด ตัวเลือกนี้ระบุว่า export ใดถูกใช้จริงและ export ใดถูกส่งออกเท่านั้น (provided) การรวมกันของ usedExports และ Terser ให้ประสิทธิภาพสูงสุดในการกำจัดโค้ดที่ตายแล้ว
// webpack.config.js — การกำหนดค่า Tree Shaking
module.exports = {
mode: "production",
entry: "./src/app.js",
output: {
filename: "bundle.js",
},
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules\/(?!(my-lib)\/).*/,
use: {
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", { modules: false }],
],
},
},
},
],
},
};พารามิเตอร์สำคัญคือ modules: false ใน @babel/preset-env โดยค่าเริ่มต้น Babel จะแปลงโมดูล ES เป็น CommonJS ซึ่งทำให้ Tree Shaking ไร้ผล modules: false ป้องกันไม่ให้ Babel แปลง import/export คงไว้ซึ่งไวยากรณ์ ES สำหรับ Webpack concatenateModules ยังรวมโมดูลเข้าสู่ขอบเขตที่ใช้ร่วมกัน ลดจำนวน IIFE และลดขนาด bundle
Side effects (ผลข้างเคียง) — การกระทำที่โมดูลดำเนินการเมื่อถูกนำเข้าซึ่งไม่เกี่ยวข้องกับค่าที่ส่งออก: สไตล์ทั่วโลก (import "./styles.css") polyfills (import "core-js/stable") การเริ่มต้นตัวแปรทั่วโลกหรือการลงทะเบียน Service Worker ถ้าโมดูลมีผลข้างเคียง ตัวรวมบัณฑลจะไม่สามารถลบโมดูลออกจาก bundle ได้อย่างปลอดภัย แม้ว่าจะไม่มี export ใดถูกใช้ก็ตาม
แฟล็ก sideEffects ใน package.json บอกตัวรวมบัณฑลว่าโมดูลใดในแพ็กเกจไม่มีผลข้างเคียง สำหรับแพ็กเกจที่โมดูลทั้งหมดบริสุทธิ์ (ส่งออกเฉพาะฟังก์ชัน) ควรระบุ "sideEffects": false สำหรับแพ็กเกจที่มี CSS หรือ polyfills — อาร์เรย์ของเส้นทางไปยังไฟล์ที่มีผลข้างเคียง: "sideEffects": ["*.css"] หากไม่มีแฟล็กนี้ Tree Shaking จะไม่ลบแม้แต่ฟังก์ชันที่ไม่ได้ใช้
เพื่อตรวจสอบว่าโมดูลมีผลข้างเคียงหรือไม่ ให้ถามคำถามนี้: import นี้จะดำเนินการใด ๆ ที่ไม่เกี่ยวข้องกับการส่งออกค่าหรือไม่ import "./styles.css" เพิ่ม CSS ลงใน DOM — นั่นคือผลข้างเคียง import { throttle } from "lodash-es" ไม่มีผลข้างเคียง — มันแค่ทำให้ฟังก์ชัน throttle พร้อมใช้งาน Polyfills (import "core-js/stable") มีผลข้างเคียง — มันปรับเปลี่ยนต้นแบบทั่วโลก
สำหรับโมดูลของคุณเอง แนะนำให้: แยกสไตล์และ polyfills ออกเป็นจุดเริ่มต้นแยกต่างหาก แยกยูทิลิตี้บริสุทธิ์ (ฟังก์ชันที่ไม่มีผลข้างเคียง) ออกจากโมดูลที่มีผลข้างเคียง (การเริ่มต้น การบันทึก การลงทะเบียน Service Worker) ใน package.json ของโปรเจกต์ระดับบนสุด ให้ระบุ "sideEffects": false เฉพาะเมื่อโมดูลทั้งหมดบริสุทธิ์ ถ้ามีสไตล์ ให้ระบุ "sideEffects": ["*.css"] อย่างแม่นยำ
{
"name": "my-ui-lib",
"version": "2.1.0",
"sideEffects": [
"*.css",
"polyfills.js"
],
"module": "dist/index.esm.js",
"main": "dist/index.cjs.js"
}"sideEffects": ["*.css", "polyfills.js"] หมายถึง: ไฟล์ CSS ทั้งหมดมีผลข้างเคียง (ไม่สามารถลบได้) และ polyfills.js ก็เช่นกัน ไฟล์ JS อื่น ๆ ทั้งหมดในแพ็กเกจบริสุทธิ์ — สามารถ shake ได้อย่างปลอดภัย ฟิลด์ module ระบุเส้นทางไปยังเวอร์ชัน ES ของแพ็กเกจที่ตัวรวมบัณฑลควรใช้แทนเวอร์ชัน CommonJS (main) สำหรับ Tree Shaking
React Native กับ Metro Bundler รองรับ Tree Shaking เวอร์ชันจำกัด Metro ไม่ดำเนินการวิเคราะห์แบบคงที่สมบูรณ์ของ export ที่ใช้แล้ว (usedExports) เช่น Webpack แต่ Metro อาศัย Terser เพื่อลบส่วนที่ไม่ได้ใช้ของโมดูลในระหว่างการย่อขนาด ประสิทธิภาพของวิธีการนี้ต่ำกว่า Tree Shaking แบบเต็มใน Webpack
สำหรับการเพิ่มประสิทธิภาพสูงสุดของโปรเจกต์ React Native แนะนำให้: ใช้ไลบรารีที่มีโมดูล ES (ฟิลด์ module ใน package.json) เพิ่ม babel-plugin-transform-remove-console เพื่อลบโค้ดดีบัก และกำหนดค่า Metro transformer.minifierConfig สำหรับ Terser นอกจากนี้ Ram Bundle (การแบ่ง bundle ออกเป็นโมดูล) ช่วยลดการโหลดหน้าจอที่ไม่ได้ใช้
คำถามที่พบบ่อย
CommonJS (require/module.exports) ไม่รองรับการวิเคราะห์แบบคงที่ — require สามารถถูกเรียกแบบไดนามิกภายในเงื่อนไขและฟังก์ชัน ตัวรวมบัณฑลไม่สามารถระบุได้ว่าส่วนใดของโมดูลถูกใช้จริง เฉพาะโมดูล ES ที่มี import/export แบบคงที่เท่านั้นที่เปิดใช้งาน Tree Shaking ได้
TypeScript เข้ากันได้อย่างสมบูรณ์กับ Tree Shaking โดยมีเงื่อนไขว่า tsconfig.json ถูกกำหนดค่าสำหรับโมดูล ES: "module": "esnext" คอมไพเลอร์ TypeScript ต้องคง import/export ไว้โดยไม่แปลงเป็น CommonJS Babel กับ @babel/preset-typescript และ modules: false ก็ส่งโมดูล ES ไปยัง Webpack อย่างถูกต้อง
Webpack Bundle Analyzer — ปลั๊กอินที่แสดงองค์ประกอบ bundle เป็นแผนภาพแบบโต้ตอบ ถ้าไลบรารีปรากฏใน bundle แต่ฟังก์ชันของมันไม่ได้ถูกใช้ แสดงว่า Tree Shaking ไม่ทำงาน คุณยังสามารถวิเคราะห์ไฟล์เอาต์พุต: ค้นหา export ที่ไม่ได้ใช้ในข้อความ bundle ผ่าน grep
Lodash v4 ถูกเผยแพร่เป็นแพ็กเกจ CommonJS สำหรับ Tree Shaking คุณต้องใช้ lodash-es — เวอร์ชัน ES ของไลบรารี แทนที่ import throttle from "lodash/throttle" ด้วย import { throttle } from "lodash-es" และกำหนดค่า resolve.alias ใน Webpack เพื่อแทนที่ lodash ด้วย lodash-es
Tree Shaking เพิ่มเวลาในการสร้างเล็กน้อย (5–15%) เพราะเพิ่มขั้นตอนการวิเคราะห์กราฟการพึ่งพาและการทำเครื่องหมาย export ที่ใช้แล้ว ในโหมด development โดยทั่วไป Tree Shaking จะถูกปิดเพื่อความเร็ว ใน production เวลาที่เพิ่มขึ้นนั้นคุ้มค่ากับการลดขนาด bundle อย่างมีนัยสำคัญ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม