Series: JavaScript Basics Lesson 75

Tree Shaking

Tree shaking nima ekani: bundler statik tahlil orqali ishlatilmagan eksportlarni qanday belgilab o'chirishi (Garbage Collection'dagi reachability'ga o'xshash), side effects muammosi va package.json'dagi sideEffects flag, Webpack/Rollup/esbuild farqi, va V8'ning bunga aloqasi.

75-dars

Tree Shaking

Tree shaking — bundler (Webpack, Rollup, esbuild, Vite) tomonidan amalga oshiriladigan jarayon: kodingizda yozilgan, lekin hech qayerda ishlatilmagan eksportlarni aniqlab, ularni yakuniy bundle fayldan butunlay olib tashlash.

Nomi qiziq metaforadan kelgan: dastur — bu daraxt (dependency tree), va uni "silkitsangiz" (shake), kerak bo'lmagan, "quruq barglar" (ishlatilmagan kod) yerga tushib qoladi, faqat kerakli, tirik shoxlar qoladi.

// utils.js
export function formatDate(d) { /* ... */ }
export function formatCurrency(n) { /* ... */ }
export function formatPhone(n) { /* ... */ }

// main.js
import { formatDate } from './utils.js';
formatDate(new Date());

Agar loyihada faqat formatDate ishlatilsa, bundler formatCurrency va formatPhoneni final bundle'ga umuman qo'shmaydi — garchi ular utils.js faylida yozilgan bo'lsa ham.


1. Nega bu kerak — real sabab

Katta loyihalarda ko'pincha butun kutubxona import qilinadi, lekin undan bitta-ikkita funksiya ishlatiladi:

import _ from 'lodash'; // butun lodash — yuzlab funksiya
_.debounce(fn, 300);    // faqat bitta funksiya kerak

Tree shaking bo'lmasa, foydalanuvchi brauzeriga butun lodash kutubxonasi (yuklab olinadigan JS hajmi sifatida) yuboriladi, garchi undan 1% ishlatilsa ham. Bu — sahifa yuklanish tezligiga, ayniqsa mobil tarmoqda, jiddiy salbiy ta'sir qiladi.


2. Bu qanday ishlaydi — bundler darajasida, qadam-baqadam

1-qadam: Statik tahlil

Bundler avval butun loyihani parse qilib, modul grafigini quradi — qaysi fayl qaysi faylni import qiladi, va har bir faylda aniq qaysi nomlar import/export qilinadi. Bu faqat ESM sintaksisi (import/export) bilan ishonchli ishlaydi, chunki u statik — CommonJS'ning require()i bilan bu tahlil ishonchsiz (oldingi mavzuda tushuntirilgan sabab).

2-qadam: "Kim ishlatilyapti" belgilash (Mark)

Bundler main.jsdan boshlab, haqiqatan chaqirilgan/ishlatilgan eksportlarni kuzatib boradi:

main.js → import { formatDate } → utils.js.formatDate ✅ ISHLATILGAN
utils.js.formatCurrency → hech kim import qilmagan ❌ ISHLATILMAGAN
utils.js.formatPhone → hech kim import qilmagan ❌ ISHLATILMAGAN

Bu — klassik graph traversal (grafni aylanib chiqish) algoritmi: main.jsdan boshlab, "reachable" (yetib boriladigan) barcha kod belgilanadi. Bu, aslida, avval o'rgangan Garbage Collection'dagi "reachability" g'oyasiga juda o'xshash — faqat bu safar xotiradagi obyektlar emas, balki fayldagi kod bo'laklari "reachable" yoki "unreachable" deb belgilanadi.

3-qadam: O'chirish (Dead Code Elimination — DCE)

Belgilanmagan (unreachable) kod bundle yaratish jarayonida butunlay tashlab yuboriladi. Bu — minifikatsiyadan oldin yoki birga ishlaydigan alohida bosqich.


3. Nega har doim ham ideal ishlamaydi — "Side Effects" muammosi

Bu yerda eng nozik va muhim tushuncha keladi. Bundler har doim ham kodni xavfsiz o'chira olmaydi, chunki ba'zi kodning yon ta'siri (side effect) bo'lishi mumkin — ya'ni, funksiya export qilinmasa ham, uning shunchaki mavjudligi (fayl yuklanishi) dasturga ta'sir qilishi mumkin:

// polyfills.js
Array.prototype.myCustomMethod = function () { /* ... */ };
console.log('Polyfill yuklandi');

export function helper() { /* ... */ }

Agar boshqa fayl faqat helperni import qilsa-yu, helper ishlatilmasa, bundler bu faylni "keraksiz" deb hisoblab, butun faylni olib tashlashi mumkin edi. Lekin bu xato bo'lardi — chunki fayl ichidagi Array.prototype.myCustomMethod = ... qatori export qilinmagan bo'lsa ham, fayl yuklanganda avtomatik ishga tushadi va dasturning boshqa joylariga ta'sir qiladi.

Shuning uchun bundlerlar konservativ yondashadi: agar biror faylda import/export doirasidan tashqarida top-level side effect (masalan, global obyektni o'zgartirish, console.log, DOM manipulyatsiya) bo'lsa, bundler bu faylni butunlay o'chirishga jur'at etolmaydi — xatoga yo'l qo'ymaslik uchun.


4. package.jsondagi sideEffects flag

Aynan shu noaniqlikni hal qilish uchun, kutubxona mualliflari package.jsonga maxsus signal qo'shadi:

{
  "name": "my-library",
  "sideEffects": false
}

Bu — dasturchining bundlerga bergan va'dasi: "Bu kutubxonadagi hech bir modulda import/export doirasidan tashqari yon ta'sir yo'q — xotirjam bo'l, ishlatilmagan har qanday faylni yoki eksportni xavfsiz o'chirsang bo'ladi."

Agar ba'zi fayllar haqiqatan side-effect'li bo'lsa (masalan, CSS import qiluvchi fayl), ularni istisno qilib ko'rsatish mumkin:

{
  "sideEffects": ["./src/polyfills.js", "*.css"]
}

Bu — nega ba'zi npm kutubxonalar (masalan, lodash-es) tree-shakeable, ba'zilari (eski, CommonJS bilan yozilgan kutubxonalar) esa umuman tree-shake qilinmaydi — muallif bu flag'ni to'g'ri sozlamagan yoki kutubxona CommonJS'da yozilgan bo'lsa.


5. Real bundlerlar darajasida farq: Webpack vs Rollup vs esbuild

  • Rollup — tree shaking g'oyasini birinchi mashhur qilgan bundler. U har bir statement darajasida tahlil qiladi (nafaqat fayl darajasida) — hatto bitta faylning ichidagi ishlatilmagan qismini ham ajratib olishga harakat qiladi.
  • Webpack (versiya 2+) — xuddi shu g'oyani qo'llaydi, lekin tarixan ancha "konservativ" bo'lgan (side-effect tekshiruvida ehtiyotkorroq), keyingi versiyalarda yaxshilangan.
  • esbuild — Go tilida yozilgan, juda tez ishlaydigan bundler; tree shaking'ni AST darajasida, minimal overhead bilan amalga oshiradi, shuning uchun katta loyihalarda sezilarli tezroq.

6. V8'ning o'zi bilan bog'liqligi qancha?

Muhim aniqlik: Tree shaking — bu V8'ning o'zi qiladigan narsa emas, balki build-time (bundler) jarayoni. V8 runtime'da faqat unga berilgan (allaqachon "silkitib" tozalangan) faylni oladi va ishga tushiradi — u "bu funksiya ishlatilmagan edi" deb bilmaydi ham, chunki u kod umuman bundle'da yo'q.

Lekin V8'ning o'zida ham shunga o'xshash, lekin boshqa maqsaddagi mexanizm bor — Dead Code Elimination JIT kompilyatorda (masalan, o'tgan TurboFan): agar funksiya ichida if (false) { ... } kabi hech qachon bajarilmaydigan shoxobcha bo'lsa, JIT optimallashtirish paytida shu shoxobchani kompilyatsiya qilinadigan mashina kodidan olib tashlaydi. Lekin bu — runtime'da, faqat bitta funksiya ichida, execution paytida sodir bo'ladi; tree shaking esa build vaqtida, butun modul grafigi darajasida ishlaydi. Ikkalasi ham "kerak bo'lmagan kodni yo'q qilish" g'oyasiga asoslangan, lekin butunlay boshqa bosqichlarda, boshqa vositalar tomonidan amalga oshiriladi.