Sorun genellikle tek bir kötü karar değildir. Lodash'ın tamamını import etmek, kullanılmayan icon setleri, her sayfada yüklenen ama yalnızca bir formda ihtiyaç duyulan bir tarih kütüphanesi — her biri tek başına küçük görünür. Birikim fark yaratır. Üstelik bu şişme çoğunlukla yavaş ilerler: proje büyüdükçe, ekip değiştikçe, bağımlılıklar güncellendikçe bundle da büyür. Kimse alarm vermez çünkü tek bir commit suçlu değildir.

Kontrolü geri almak için önce mevcut durumu ölçmek, sonra hangi kütüphanenin ne kadar yer kapladığını görmek ve ardından yükü kritik yol dışına taşımak gerekir. Bu üç adım birbiriyle bağlantılıdır; birini atlamak diğerini verimsiz kılar.

Bundle içeriğini görünür kılmak: analiz araçları

Neyi küçülteceğinizi bilmeden boyut optimizasyonu yapamazsınız. Create React App kullanan projelerde source-map-explorer, Vite projelerinde rollup-plugin-visualizer, Webpack tabanlı yapılarda ise webpack-bundle-analyzer bu görünürlüğü sağlar. Üçü de benzer bir çıktı üretir: her modülün bundle içindeki boyutunu ve hangi paket tarafından dahil edildiğini gösteren görsel bir harita.

Analizi yaparken dikkat edilmesi gereken birkaç nokta var. Gzip sonrası boyutlar ham boyutlardan farklıdır; araçların çoğu ham boyutu gösterir, gerçek ağ maliyetini görmek için sıkıştırılmış çıktıya bakmak gerekir. Ayrıca bundle analizinin production build üzerinde yapılması şarttır — development modunda tree shaking ve minification devrede değildir, rakamlar yanıltıcı olur.

Haritada genellikle birkaç şey öne çıkar: beklenenin çok üzerinde yer kaplayan bir kütüphane, aynı kütüphanenin farklı versiyonlarının ikiye katlanması ya da bir sayfaya ait modüllerin ana chunk'a sızması. Bu üç pattern en sık karşılaşılan şişme kaynaklarıdır ve hepsinin farklı çözümü vardır.

Bağımlılık seçiminin bundle üzerindeki ağırlığı

React ekosisteminde pek çok kütüphanenin ağır bir fiyatı var. moment.js tüm locale verisiyle birlikte gzip sonrası yaklaşık 67 KB yer kaplar; aynı işi yapan date-fns yalnızca kullandığınız fonksiyonları dahil eder ve çoğu projede bu 5–10 KB'a iner. lodash'ın tamamını import etmek yerine yalnızca ihtiyaç duyulan fonksiyonu almak benzer bir fark üretir.

Sorun yalnızca büyük kütüphaneler değildir. Küçük görünen paketler de ağır bağımlılıkları beraberinde getirebilir. Bir paketin gerçek maliyetini görmek için bundlephobia.com gibi araçlar ya da doğrudan analiz haritası kullanılabilir. Bir UI bileşen kütüphanesi seçerken tree-shakeable olup olmadığını kontrol etmek, ilerleyen dönemde ciddi fark yaratır: tüm kütüphaneyi tek parça olarak bundle'a dahil eden yapılar ile yalnızca kullanılan bileşenleri dahil edenler arasında on katlık boyut farkı görülebilir.

Tree shaking bu noktada devreye girer, ancak her kütüphane için otomatik çalışmaz. CommonJS (require) formatındaki paketler Webpack ve Vite tarafından shaken edilemez; ES Modules (import/export) kullanan paketler edilebilir. package.json'da "sideEffects": false ya da belirli dosyaların listesi olmayan bir kütüphanede tree shaking güvenilir şekilde çalışmaz. Bu nedenle bağımlılık seçimi hem boyut hem de format açısından değerlendirilmelidir.

Code splitting ile yükü rotaya dağıtmak

Her sayfanın kodunu tek bir bundle'da göndermek, kullanıcının hiçbir zaman görmeyeceği sayfaların JavaScript'ini de indirtmek anlamına gelir. Code splitting bu yaklaşımı tersine çevirir: her rota ya da özellik kendi chunk'ına ayrılır ve yalnızca ihtiyaç duyulduğunda yüklenir.

React'te en yaygın yöntem React.lazy ve Suspense kombinasyonudur. Rota bileşenlerini bu yapıyla sarmak, Webpack ve Vite'ın otomatik olarak ayrı chunk üretmesini sağlar:

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Reports = lazy(() => import('./pages/Reports'));

function App() {
  return (
    <Suspense fallback={<div>Yükleniyor...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/reports" element={<Reports />} />
      </Routes>
    </Suspense>
  );
}

Bu yapıyla ana chunk yalnızca routing mantığını ve ortak kodu taşır; her sayfanın kodu o sayfaya gelindiğinde yüklenir. Büyük bir uygulama için ilk yükten 200–400 KB arasında bir kazanım alışılmadık değildir. Rota düzeyinde splitting yanı sıra ağır bileşenler için de aynı teknik uygulanabilir: bir PDF önizleme kütüphanesi, büyük bir grafik bileşeni ya da yalnızca admin kullanıcılarının gördüğü bir panel bu kategoriye girer.

Dynamic import bileşen bazında daha ince kontrol imkânı verir. Bir butona tıklandığında ya da bir event tetiklendiğinde modülü yüklemek, sayfanın geri kalanını bloke etmeden ihtiyaç duyulana kadar yükü ertelemenin en doğrudan yoludur.

Vendor chunk stratejisi ve chunk sınırları

Tüm node_modules'ü tek bir vendor.js dosyasına toplamak yaygın bir varsayılan yapıdır. Bu yaklaşımın bir cazibesi var: uygulama kodu değişse bile büyük vendor chunk önbellekte kalır. Ama aynı zamanda bir tuzağı da var: React, ReactDOM ve react-router gibi sık değişmeyen paketlerle birlikte, bağımlılıklar güncellenince tüm vendor chunk geçersiz olur.

Daha sağlıklı bir strateji vendor chunk'ı değişim hızına göre bölmektir. Neredeyse hiç güncellenmeyen stabil kütüphaneler (React çekirdeği, ReactDOM) ayrı bir chunk'ta tutulur ve çok uzun cache süresiyle sunulur. Sık güncellenen veya özelliğe özgü kütüphaneler ise ayrı chunk'lara taşınır. Vite'ta bu build.rollupOptions.output.manualChunks ile; Webpack'te optimization.splitChunks ile yapılandırılır.

Chunk boyutu da önemlidir. Çok fazla küçük chunk HTTP/2 ile pratikte sorun yaratmaz, ama aşırıya kaçmak browser'ın chunk birleştirme maliyetini artırır. Genellikle 20–50 KB (gzip sonrası) aralığındaki chunk'lar hem cache verimliliği hem de paralel yükleme açısından dengeli bir nokta olarak kabul edilir. Bu eşiğin altındaki fragment chunk'lar birleştirmeye değer olabilir.

Sürekli izleme: boyut regresyonunu yakalamak

Bundle boyutu bir kez düzeltip bırakılacak bir şey değildir. Her yeni bağımlılık ekleme, her paket güncellemesi potansiyel bir regresyon kaynağıdır. Bunu kontrol altında tutmanın en pratik yolu build çıktısına boyut sınırı koymak ve bu sınırı CI sürecine entegre etmektir.

Vite'ta build.chunkSizeWarningLimit ile belirli bir eşiğin üstündeki chunk'lar için uyarı alınır. Webpack'te performance.hints ve performance.maxAssetSize benzer işlevi görür. Ancak bunlar yalnızca uyarı üretir; hard limit için bundlesize veya size-limit gibi araçlar CI pipeline'a eklenerek her PR'da boyut kontrolü yapılabilir:

// package.json
"size-limit": [
  {
    "path": "dist/assets/index-*.js",
    "limit": "150 kB"
  },
  {
    "path": "dist/assets/vendor-*.js",
    "limit": "200 kB"
  }
]

Bu yapı kurulduktan sonra bir PR'ın bundle boyutunu 50 KB artırması otomatik olarak fark edilir. Kimin neyi eklediği netleşir, tartışma somutlaşır. Boyut bütçesi aynı zamanda bağımlılık seçimini de disipline eder: "bu kütüphaneyi ekleyebilir miyiz?" sorusu sayısal bir yanıta kavuşur.

Bağımlılık denetimi de bu sürecin bir parçasıdır. npm ls ile duplicate paket versiyonları görülebilir; aynı kütüphanenin iki farklı versiyonu bundle'da iki kez yer alıyorsa bu hem boyutu hem de davranışı etkiler. depcheck ile kullanılmayan bağımlılıklar tespit edilebilir. Kullanılmayan kodun temizlenmesi kütüphane düzeyinde de aynı mantıkla işler.

Bundle boyutunu kontrol altında tutmak bir seferlik bir iyileştirme değil, süregelen bir disiplindir. Analiz haritasını düzenli okumak, yeni bağımlılık eklerken boyut etkisini sorgulamak ve CI'da boyut sınırlarını aktif tutmak bu disiplinin temel pratikleridir. Parse ve execute maliyetinin doğrudan kullanıcı deneyimine yansıdığını düşündüğünüzde, büyük bir bundle'ın yalnızca indirme sorunu olmadığı netleşir — her kilobayt, yavaş cihazlarda işlemci üzerinde karşılık bulur.

Küçük, odaklı adımlar birikimlere yol açar. Bir rotayı lazy loading'e taşımak, ağır bir kütüphaneyi hafif alternatifiyle değiştirmek, boyut bütçesini CI'a eklemek — her biri tek başına mütevazı ama birlikte düzinelerce kilobayt ve saniyenin altında ölçülen yükleme kazanımı sağlar.