Bir kütüphaneden tek bir fonksiyon içeri alıyorsun ama build çıktısında o kütüphanenin tamamını görüyorsan, tree shaking beklendiği gibi çalışmıyor demektir. Tree shaking, bundler'ın "bu kod hiçbir yerden çağrılmıyor" diyerek ölü modülleri nihai çıktının dışında bırakması işlemidir. Adı ağacı silkelemekten gelir: bağımlılık ağacını sallarsın, tutunamayan yapraklar düşer.
Bu yazıda tree shaking'in tam olarak neyi silip neyi silemediğini, neden CommonJS ile çalışmadığını, package.json içindeki sideEffects bayrağının ne işe yaradığını ve pratikte ölü kodun neden bundle'da kalmaya devam ettiğini anlatacağım. Ayrıca kendi kodunu tree shaking'e uygun yazmanın somut kurallarını ve sonucun gerçekten silindiğini nasıl doğrulayacağını göstereceğim.
Tree Shaking Tam Olarak Ne Yapar#
Bundler, giriş dosyandan başlayarak tüm import ifadelerini takip eder ve bir bağımlılık grafiği kurar. Tree shaking bu grafiği bir adım öteye taşır: yalnızca dosya düzeyinde değil, dışa aktarılan tekil isimler düzeyinde hangi kodun gerçekten kullanıldığını hesaplar. Bir modül on fonksiyon dışa aktarıyor ama sen ikisini kullanıyorsan, kalan sekizi çıktıya yazılmaz.
İşin özü statik analizdir; yani bundler kodu çalıştırmadan, sadece okuyarak karar verir. Bu, tree shaking'in hem gücünü hem sınırını açıklar. Bundler bir fonksiyonun çağrılıp çağrılmadığını okuyarak anlayabilir, ama bir dizinin çalışma anında hangi anahtarla erişileceğini bilemez. Aşağıdaki tablo neyin silinebildiğini özetliyor:
| Durum | Silinebilir mi | Neden |
|---|---|---|
| Kullanılmayan named export | Evet | Statik olarak izlenebilir |
| Kullanılmayan tüm modül | Evet | Grafiğe hiç girmez |
import * as X ile alınıp bir kez kullanılan modül | Kısmen | Bundler'a ve kullanıma bağlı |
| Yan etkisi olan modül (global'e yazan) | Hayır | Silmek davranışı bozar |
CommonJS require() ile alınan | Genelde hayır | Statik analiz edilemez |
Çalışma anında obj[degisken] ile erişilen | Hayır | Hangi isim olduğu bilinemez |
Bir noktayı netleştirmekte fayda var: tree shaking, kullanılmayan kodu çıktıdan çıkarır, bölünüp sonraya ertelenmesini sağlamaz. Ertelemek istediğin kod için ayrı bir mekanizma vardır ve o code splitting başlığı altındadır. İkisi birbirinin alternatifi değil, tamamlayıcısıdır.
Neden ESM Zorunlu#
Tree shaking'in çalışması için modül sisteminin statik olması gerekir. ES modülleri (import / export) tam olarak bunu sağlar: bir dosyanın neyi içeri aldığı ve neyi dışa verdiği derleme anında bellidir, koşullu olamaz, çalışma anında değişemez. CommonJS (require / module.exports) ise bir fonksiyon çağrısıdır; if içinde olabilir, yolu değişkenden gelebilir, dışa verilen nesne çalışma anında değiştirilebilir.
// ESM: derleme anında kesin bilinir
import { debounce } from "./yardimcilar.js";
// CommonJS: çalışma anında çözülür, bundler tahmin edemez
const { debounce } = require("./yardimcilar.js");
if (kosul) {
Object.assign(module.exports, require("./ekstra.js"));
}
Bu yüzden bir paketin tree shaking'e uygun olup olmadığını anlamanın en hızlı yolu package.json dosyasına bakmaktır. Paket bir module veya exports alanı üzerinden ESM sürümü sunuyorsa bundler o sürümü tercih eder ve ölü kodu ayıklayabilir; yalnızca main ile CommonJS sunuyorsa ayıklama büyük ölçüde devre dışı kalır.
# Bir paketin ESM sürümü sunup sunmadığını kontrol et
cat node_modules/paket-adi/package.json | grep -E '"(main|module|exports|sideEffects|type)"'
Çıktıda "module" ya da "type": "module" görüyorsan iyi durumdasın. Yalnızca "main" varsa ve paket büyükse, aynı işi yapan ESM sunan bir alternatif aramaya değer; bu tek karar çoğu zaman tüm diğer optimizasyonlardan fazla kazandırır.
sideEffects Bayrağı#
Bundler'ın karşılaştığı ikilem şudur: bir modül dışa aktardığı hiçbir şey kullanılmasa bile, yalnızca içeri alınmış olmakla bir iş yapıyor olabilir. Bir CSS dosyası içeri alınmıştır, bir polyfill global nesneye yazmıştır, bir kütüphane kendini kaydettirmiştir. Bundler bunu bilemediği için varsayılan olarak temkinlidir ve modülü çıktıda tutar.
package.json içindeki sideEffects alanı, bu belirsizliği ortadan kaldırmak için vardır:
{
"name": "benim-kutuphanem",
"type": "module",
"sideEffects": false
}
false demek, "bu paketteki hiçbir dosya yalnızca içeri alınmakla bir iş yapmaz; kullanılmayan her şeyi rahatça silebilirsin" demektir. Ancak paketinde CSS içeri alan ya da global'e yazan dosyalar varsa false yazmak tehlikelidir; o dosyalar silinir ve stiller kaybolur. Doğru yaklaşım istisnaları listelemektir:
{
"sideEffects": ["**/*.css", "./src/polyfill.js"]
}
Kendi uygulama projende de bu alan işe yarar. Ancak dikkat: uygulamanın giriş dosyasında import "./stiller/global.css" gibi satırların varsa ve sideEffects: false yazarsan, üretim build'inde stillerin kaybolduğunu görürsün. Bu, hata mesajı vermeyen ve build'i kırmayan bir sorundur; yalnızca sonuca bakarak fark edilir.
Tree Shaking'i Bozan Kalıplar#
Pratikte "tree shaking çalışmıyor" şikayetinin arkasında neredeyse her zaman aşağıdaki kalıplardan biri vardır.
Namespace import kullanmak. import * as X from "paket" yazdığında, bundler'ın X nesnesinin hangi özelliklerine erişileceğini takip etmesi gerekir. Modern bundler'lar bunu bir dereceye kadar yapar ama nesne bir fonksiyona geçirilir geçirilmez iz kaybolur ve modülün tamamı korunur.
// SORUNLU: tüm modül grafiğe girer
import * as yardimcilar from "./yardimcilar.js";
export function kur(ayarlar) {
return uygula(yardimcilar, ayarlar); // iz burada kayboldu
}
// DOĞRU: yalnızca gerçekten kullanılanlar
import { debounce, throttle } from "./yardimcilar.js";
Barrel dosyaları. Bir index.js içinde onlarca modülü yeniden dışa aktarmak (export * from "./a") geliştirici deneyimi açısından hoştur ama bundler'ı zorlar; özellikle o modüllerin bir kısmının yan etkisi varsa hepsi çıktıda kalır. Büyük projelerde barrel yerine doğrudan dosya yolundan içeri almak daha küçük çıktı verir.
Sınıf yerine nesne genişletmek. Prototipe çalışma anında metod ekleyen kalıplar statik analizle izlenemez; bundler hangi metodun kullanıldığını bilemez ve hepsini tutar.
Transpiler'ın modülleri CommonJS'e çevirmesi. Derleyici yapılandırmanda modül hedefi CommonJS olarak ayarlıysa, ESM yazmış olsan bile bundler'a ulaşan kod CommonJS olur ve ayıklama devre dışı kalır. Bu, en sinsi sebeplerden biridir çünkü kaynak kodun tamamen doğru görünür.
Geliştirme build'ine bakmak. Tree shaking yalnızca üretim modunda çalışır. npm run dev çıktısında ölü kod görmen tamamen normaldir.
Bundler Tarafındaki Ayarlar#
Tree shaking'i "açan" tek bir düğme yoktur; birkaç ayarın birlikte doğru olması gerekir ve yanlış olan tek bir tanesi ayıklamayı sessizce devre dışı bırakır. En kritik olanı, kaynak kodun bundler'a ESM olarak ulaşmasıdır. TypeScript veya Babel kullanıyorsan modül hedefini CommonJS'e çeviren ayarı kapatman gerekir; aksi halde kaynağında import yazsan da bundler'a require ulaşır.
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES2020"
}
}
Babel tarafında aynı tuzak @babel/preset-env içindeki modül ayarındadır; modules: false vermezsen preset her şeyi CommonJS'e dönüştürür ve tree shaking biter.
// babel.config.js
module.exports = {
presets: [["@babel/preset-env", { modules: false, targets: "> 0.5%, not dead" }]],
};
İkinci ayar, üretim modunun gerçekten etkin olmasıdır. webpack'te mode: "production", Vite ve Rollup'ta ise npm run build komutunun kendisi bunu sağlar. Üçüncüsü, minify aşamasının çalışmasıdır: bundler ölü modülü grafikten çıkarır ama nihai temizliği minifier yapar, dolayısıyla minify kapalıysa çıktıda kalan izleri görürsün.
| Ayar | Yanlış değer | Sonucu |
|---|---|---|
| Transpiler modül hedefi | commonjs | Ayıklama tamamen devre dışı |
| Build modu | development | Ölü kod çıktıda kalır |
| Minify | Kapalı | Ayıklanan kodun kalıntıları görünür |
| Paketin modül alanı | Yalnızca main | Paket bütün olarak paketlenir |
Bu dört satırı bir kontrol listesi gibi geçmek, "neden çalışmıyor" sorusunun cevabını çoğu projede birkaç dakikada verir.
Doğrulama: Gerçekten Silindi mi#
Varsayımla ilerlemek yerine çıktıyı doğrudan sorgula. En basit yöntem, silinmesini beklediğin fonksiyonun adını üretim çıktısında aramaktır.
# Üretim build'i al
npm run build
# Silinmesini beklediğin dışa aktarımın adı çıktıda var mı?
grep -c "nadirKullanilanFonksiyon" dist/assets/*.js
# Bir paketin adına özgü bir dize hâlâ paketlenmiş mi?
grep -l "AGIR_KUTUPHANE_SURUM" dist/assets/*.js
Minify sonrası fonksiyon adları kısaltıldığı için bu arama her zaman kesin sonuç vermez; o yüzden asıl güvenilir yöntem bundle analiz haritasıdır. Görselleştirme eklentisiyle üretilen haritada, kullanmadığını düşündüğün bir paketin hâlâ yer kapladığını görürsen sebebini araştırırsın. Analiz kurulumunun ayrıntıları bundle boyutunu küçültme yazısında.
Bir de kontrollü deney yöntemi vardır ve şüpheyi kesin olarak giderir: ilgili import satırını geçici olarak yorum satırı yap, build al ve çıktı boyutunu karşılaştır. Boyut belirgin biçimde düşüyorsa o kod gerçekten paketleniyordu; hiç değişmiyorsa zaten ayıklanıyordu.
Beklentini de gerçekçi tut: tree shaking bir kütüphanenin iç yapısı elverişli olduğunda yüzde seksenini bırakabilir, elverişsiz olduğunda ise hiçbir şey yapamaz. Yani kazanç kütüphaneden kütüphaneye değişir ve tek bir ölçüm bütün proje için genellenemez. Şüphelendiğin her bağımlılığı ayrı ayrı, kontrollü deney yöntemiyle sınamak en doğru sonucu verir.
Kendi Kodunu Ayıklanabilir Yazmak#
Tree shaking'ten yararlanmak yalnızca kütüphane seçmekle ilgili değil; kendi kodunun da statik analize elverişli olması gerekir. Birkaç somut kural:
- Modülleri named export ile dışa aktar; tek bir büyük varsayılan nesne dışa vermek ayıklamayı imkânsızlaştırır.
- Modül gövdesinde yan etki üretme. Dosya içeri alındığında bir şey çalışıyorsa (istek atmak, global'e yazmak, dinleyici bağlamak) o modül asla silinemez.
- Yapılandırmayı modül gövdesinde değil, çağrıldığında çalışan bir fonksiyonda kur.
- Koşullu içeri almalar için dinamik
import()kullan;require'ı koşula sokma. - Paket olarak yayınlıyorsan
sideEffectsalanını doğru doldur ve ESM sürümü sun.
// KÖTÜ: modül gövdesinde yan etki var, silinemez
import { analitik } from "./analitik.js";
analitik.baslat(); // dosya içeri alınır alınmaz çalışır
export function olayGonder(ad) { analitik.gonder(ad); }
// İYİ: hiçbir şey kendiliğinden çalışmıyor
import { analitik } from "./analitik.js";
export function baslat() { analitik.baslat(); }
export function olayGonder(ad) { analitik.gonder(ad); }
Aynı mantığın CSS tarafındaki karşılığı da vardır; kullanılmayan stil kurallarının ayıklanmasını kullanılmayan CSS'i temizleme yazısında ele aldım. İki dünyanın ortak noktası şudur: araç ancak neyin kullanıldığını görebildiği ölçüde temizlik yapabilir.
Sıkça Sorulan Sorular#
Tree shaking otomatik mi çalışır#
Modern bundler'ların üretim modunda varsayılan olarak açıktır, yani ayrıca bir ayar yapmana gerek kalmaz. Ancak "açık olması" ile "işe yaraması" farklı şeylerdir: kullandığın paketler CommonJS sunuyorsa, kodun namespace import kullanıyorsa veya transpiler modülleri dönüştürüyorsa ayıklama fiilen devre dışı kalır. Bu yüzden sonucu her zaman üretim çıktısı üzerinden doğrulaman gerekir.
Tree shaking neden bazı paketlerde çalışmıyor#
En yaygın sebep, paketin yalnızca CommonJS sürümü sunmasıdır; bundler require tabanlı bir modülde hangi dışa aktarımın kullanıldığını güvenle belirleyemez ve tamamını korur. İkinci sebep, paketin sideEffects alanını hiç belirtmemiş olmasıdır; bu durumda bundler temkinli davranıp dosyaları silmez. Üçüncüsü, paketin iç yapısının statik analize elverişsiz olmasıdır — çalışma anında prototip genişleten ya da dinamik anahtarla erişen kalıplar gibi.
sideEffects false yazmak güvenli mi#
Yalnızca paketindeki hiçbir dosya sadece içeri alınmakla iş yapmıyorsa güvenlidir. CSS içeri alan, global nesneye yazan, polyfill uygulayan ya da bir kayıt işlemi yapan dosyaların varsa false yazmak bu dosyaların silinmesine ve sessiz hatalara yol açar. Doğru yol, istisnaları bir dizi olarak listelemektir; böylece hem ayıklama çalışır hem yan etkili dosyalar korunur.
Tree shaking ile code splitting arasındaki fark ne#
Tree shaking, hiç kullanılmayan kodu çıktının tamamen dışında bırakır — o kod hiçbir zaman, hiçbir kullanıcı tarafından indirilmez. Code splitting ise kullanılan ama hemen gerekmeyen kodu ayrı bir dosyaya alır ve gerektiğinde yükler; kod silinmez, ertelenir. İkisi farklı problemleri çözer ve birlikte uygulandığında ilk yükleme boyutu üzerinde çarpan etkisi yaratır.
Kullanılmayan kodu elle silmek daha mı iyi#
Gerçekten ölü olan kendi kodunu silmek her zaman en temiz çözümdür; hem çıktıyı hem bakım yükünü azaltır. Ancak tree shaking'in asıl değeri, silemeyeceğin kod üzerinde çalışmasıdır: bir kütüphanenin kullanmadığın yüzde doksanını sen silemezsin ama bundler bırakabilir. Yani ikisi rakip değil; kendi kodunda disiplin, bağımlılıklarda tree shaking.
Tree shaking sonrası kod bozulur mu#
Doğru yapılandırıldığında bozulmaz, çünkü yalnızca hiçbir yerden erişilmeyen kod atılır. Bozulma neredeyse her zaman yanlış bir sideEffects: false beyanından kaynaklanır: yan etkisi olan bir dosya kullanılmıyor sanılıp silinir ve stiller ya da polyfill'ler kaybolur. Bu tür bir kayıp build'i kırmaz, sadece üretimde ortaya çıkar; bu yüzden üretim build'ini yayına almadan önce yerelde açıp gezmek iyi bir alışkanlıktır.
Kapanış#
Tree shaking, kullanılmayan kodu çıktının dışında bırakan sessiz bir kazançtır ama kendiliğinden garanti değildir. Aklında tutman gereken dört alışkanlık: ESM sunan paketleri tercih et, import * as yerine adlandırılmış içeri almalar kullan, modül gövdesinde yan etki üretme ve sonucu her zaman üretim build'i üzerinde doğrula. Paket yayınlıyorsan sideEffects alanını doğru doldurmak, kütüphaneni kullanan herkese doğrudan kazanç sağlar.
Bu tür optimizasyonların meyvesini alabilmek için build sürecini kendi kontrolünde çalıştırabilmek işini kolaylaştırır. Node.js kurabileceğin tam root erişimli VDS ve bulut sunucu paketlerimiz buna uygundur; üretilen statik çıktıyı hızlı servis etmek için web hosting paketlerimiz yeterlidir, kurulum ve bakımı bize bırakmak istersen sunucu yönetimi hizmetimize göz atabilirsin.