Vite Production Build'i Optimize Eden Yapılandırmalar
Vite'ın production build'ini küçülten dört yapılandırma alanı: chunk stratejisi, minifier seçimi, CSS bölme kararı ve asset inlining eşiği. Her ayarın ne zaman kazandırdığı, ne zaman ters etki ürettiği somut örneklerle ele alınıyor.
Vite'ın varsayılan production build'i çoğu proje için zaten iyi bir sonuç verir. Ama "iyi" ile "bu proje için optimum" arasındaki fark genelde vite.config.js'e hiç dokunulmamış olmasından kaynaklanır. rollupOptions, terserOptions, cssCodeSplit, assetsInlineLimit gibi ayarlar fabrika değerleriyle bırakıldığında, orta ölçekli bir uygulamada bile chunk dağılımı verimsiz kalır, kod tekrar eder veya küçük varlıklar ana bundle'ı şişirir.
Bu yapılandırmaların hiçbiri izole bir karar değildir. Chunk stratejisi asset inlining eşiğini etkiler, o da cache davranışını belirler. Birbirinden bağımsız uygulanan optimizasyonlar zaman zaman birbirini götürür: agresif code splitting çok sayıda küçük dosya üretip istek sayısını artırabilir, agresif inlining ise önbellekleme avantajını sıfırlayabilir.
Aşağıda üretim build'ini gerçekten küçülten dört yapılandırma alanı ele alınıyor: chunk stratejisi, minification ayarları, CSS bölme kararı ve asset inlining eşiği. Her biri için hangi proje profilinde hangi değerin doğru olduğu somut örneklerle gösteriliyor.
manualChunks Vendor Kodunu Nasıl Ayırır, Ne Zaman Ters Etki Üretir?
Vite, Rollup üzerine kurulu olduğu için chunk davranışı Webpack'in modül grafiği yaklaşımından farklı çalışır: varsayılan olarak her giriş noktası kendi bağımlılıklarıyla birlikte tek bir chunk'a paketlenir, bağımlılıklar entry'ler arasında paylaşılmıyorsa aynı kütüphane birden fazla chunk'ta tekrar eder. manualChunks bu tekrarı önlemenin ve vendor kodunu uygulama kodundan ayırmanın standart yoludur.
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom')) {
return 'vendor-react';
}
if (id.includes('lodash') || id.includes('date-fns')) {
return 'vendor-utils';
}
return 'vendor';
}
}
}
}
}
});
Bu ayrımın gerçek kazancı, vendor kodunun uygulama kodundan çok daha nadir değişmesidir. Uygulama kodu her deploy'da güncellenirken vendor chunk'ı aynı hash ile önbellekte kalmaya devam eder; kullanıcı yalnızca değişen parçayı yeniden indirir. Ama fonksiyonu her paket için ayrı bir dal açacak kadar detaylandırmak ters etki üretir: onlarca küçük vendor chunk'ı, her biri ayrı bir HTTP isteği demektir ve bu isteklerin toplam overhead'i, önbellekten kazanılan süreden fazlasını yiyebilir. Doğru granülerlik, birlikte güncellenen ve gerçekten büyük olan paketleri gruplamaktır; küçük, nadiren değişen bağımlılıkları tek bir "vendor" chunk'ında toplamak çoğu projede yeterlidir.
manualChunks'ı bir nesne olarak da tanımlamak mümkündür; bu yöntem sabit eşlemeler için okunabilirliği artırır ve koşullu mantık gerektirmeyen küçük projelerde fonksiyon yerine tercih edilebilir. Fonksiyon biçimini kullanırken dikkat edilmesi gereken nokta, mono-repo yapılarında id dizesinin paket adının yanı sıra sürüm bilgisini de içermesidir; yalnızca includes('react') yazmak bazı ortamlarda beklenmedik eşleşmelere yol açabilir. Eşleşmeleri kesinleştirmek için /node_modules/react/ gibi yol sınırlayıcıları kullanmak bu tür yan etkileri önler. Hangi kütüphanelerin vendor chunk boyutuna ne kadar katkıda bulunduğunu görmek ise doğrudan kütüphane seçimindeki bundle maliyetiyle ilişkilidir; manualChunks vendor kodu ayırır ama o kodun toplam ağırlığını değiştirmez.
Terser mı esbuild mi? Minify Seçeneğinin Gerçek Maliyeti
Vite 3'ten itibaren varsayılan minifier esbuild'dir; hızlıdır ama sıkıştırma oranı terser'a göre biraz daha düşüktür. build.minify değeri 'terser' olarak değiştirildiğinde build süresi uzar, çıktı boyutu genelde yüzde birkaç daha küçülür.
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
passes: 2
},
mangle: {
safari10: true
}
}
}
drop_console ve passes: 2 gibi ayarlar terser'ın compress aşamasını iki kez çalıştırarak ek küçülme sağlar, ama build süresini de orantılı şekilde artırır. Bu fark küçük projelerde birkaç saniyeyken, büyük monorepo yapılarında CI süresine gözle görülür bir yük bindirebilir. Pratik kural şudur: dev döngüsünde ve staging deploy'larında esbuild yeterlidir, çünkü fark kullanıcıya değil geliştiriciye görünür. Public-facing bir landing page gibi her kilobaytın LCP'ye yansıdığı sayfalarda terser'a geçmek, build süresindeki artışı karşılayacak kadar anlamlı bir kazanç üretir.
CSS Code Splitting Ne Zaman Kapatılmalı?
cssCodeSplit varsayılan olarak açıktır ve her async chunk'ın kendi CSS dosyasını üretmesini sağlar. Rota bazlı lazy loading yapılan bir uygulamada bu davranış istenen sonucu verir: bir rotaya ait stiller, o rota gerçekten yüklenene kadar indirilmez.
Ama çoğu sayfada ortak bir tasarım sistemi kullanılıyorsa ve rotalar arasında paylaşılan CSS oranı yüksekse, bölme işlemi tam tersi bir sonuç üretir: aynı seçicilerin farklı chunk'larda tekrar etmesi, toplam CSS boyutunu büyütür ve her rota geçişinde yeni bir CSS dosyası isteği tetiklenir. Bu profildeki projelerde cssCodeSplit: false ayarı tüm stilleri tek dosyada toplar; ilk yüklemede biraz daha fazla CSS indirilir ama sonraki her rota geçişi ek istek gerektirmez. Hangi profilin geçerli olduğu, minify sonrası dosya boyutlarını karşılaştırarak ve chunk başına üretilen CSS'in ne kadarının ortak seçicilerden oluştuğunu inceleyerek anlaşılır.
assetsInlineLimit: Base64 Inlining Nerede Fayda Sağlar, Nerede Zarar Verir?
assetsInlineLimit varsayılan olarak 4096 bayttır; bu eşiğin altındaki görsel ve font dosyaları ayrı bir dosya olarak değil, base64 kodlanmış veri URI'si olarak doğrudan JS veya CSS içine gömülür. Küçük ikonlar için bu, ekstra bir HTTP isteğini ortadan kaldırdığı için gerçek bir kazançtır.
build: {
assetsInlineLimit: 4096 // varsayılan; proje profiline göre ayarlanabilir
}
Ama iki maliyet genelde göz ardı edilir. Birincisi, base64 kodlama dosya boyutunu yaklaşık üçte bir oranında büyütür; eşik gereğinden yüksek tutulursa, "küçük" sayılan ama aslında birkaç kilobaytlık dosyalar bundle'a gereksiz ağırlık ekler. İkincisi, gömülü bir varlık artık kendi başına önbelleklenemez; bulunduğu JS veya CSS chunk'ının içinde yaşar, o chunk'ın içerik hash'i değiştiğinde varlık da yeniden indirilir, hiçbir şey değişmemiş olsa bile. Sık güncellenen bir chunk içinde duran sabit bir logo, her deploy'da gereksiz yere yeniden indirilir. Eşiği düşürmek (örneğin 1024 bayta çekmek) yalnızca gerçekten küçük ve nadiren değişen ikonların inline kalmasını, göreceli olarak büyük ve bağımsız güncellenen görsellerin ayrı dosya olarak kalmasını sağlar.
SVG ikonlarını inline tutmak yerine sprite ya da ayrı public/ dizini tercihini yapmak da bu kararın devamıdır. public/ altına alınan bir varlık Vite'ın asset pipeline'ından geçmez, hash almaz; CDN önbelleği elle yönetilmek zorunda kalır. Küçük, değişmeyen ikonlar için inline makul bir seçim olmaya devam eder; ama tema veya marka güncellemesiyle sık değişen görseller için ayrı dosya ve doğru önbellekleme stratejisi daha az deploy karmaşıklığı üretir.
Dinamik Import ile Chunk Sınırlarını Kod Seviyesinde Kurmak
manualChunks vendor seviyesinde bir ayrım kurar, ama uygulama kodunun rota veya bileşen düzeyinde bölünmesi build config'inden değil, kod içindeki dinamik import kullanımından gelir. Vite, statik olarak import edilen her modülü ana chunk'a dahil eder; bir modül yalnızca import() ile çağrıldığında ayrı bir chunk olarak bölünür.
Build çıktısında chunkSizeWarningLimit uyarısı görüldüğünde, ilk refleks genelde bu limiti yükseltmek olur; ama bu, sorunu gizlemekten öteye geçmez. Gerçek çözüm, uyarıyı tetikleyen chunk'ın içinde hangi modüllerin durduğuna bakmak ve nadiren kullanılan büyük bağımlılıkları (bir PDF görüntüleyici, karmaşık bir grafik kütüphanesi, admin paneline özel bir bileşen) dinamik import ile ana akıştan çıkarmaktır. Bu ayrım, hangi kodun hangi kullanıcı etkileşiminde gerçekten gerekli olduğuna dair net bir karar gerektirir; rastgele her bileşeni lazy() ile sarmak, az önce ele alınan aşırı granülerlik sorununu bu kez uygulama katmanında tekrar üretir.
Yapılandırma Değişikliklerini Build Çıktısıyla Doğrulamak
Yukarıdaki dört ayardan hiçbiri sezgiyle doğrulanmamalı. rollup-plugin-visualizer veya benzeri bir araçla build öncesi ve sonrası treemap karşılaştırması, hangi paketin gerçekten yer kapladığını ve manualChunks değişikliğinin hangi dosyaları nereye taşıdığını görünür kılar.
Ölçüm yapılırken raw dosya boyutu değil, gzip veya brotli sonrası transfer boyutu esas alınmalı; çünkü sıkıştırma algoritması tekrar eden desenleri farklı şekillerde ele alır ve bazı chunk bölme kararları raw boyutta küçülme gibi görünse de sıkıştırılmış boyutta fark yaratmayabilir. Ağ sekmesinde gerçek transfer boyutunu kontrol etmek, hangi optimizasyonun kullanıcıya gerçekten ulaştığını doğrulamanın tek güvenilir yoludur.
Bu dört alan birbirinden bağımsız ayarlanmamalı. Vendor chunk'ı doğru gruplandığında CSS bölme kararı daha net görünür, asset inlining eşiği doğru kalibre edildiğinde chunk hash'leri gereksiz yere değişmez. Değişiklik sırası genelde şu şekilde işler: önce chunk stratejisi kurulur, sonra CSS bölme kararı verilir, en sonda inlining eşiği ince ayar olarak yapılır.
Her proje aynı değerlerden fayda görmez. Küçük bir landing page için varsayılan ayarlar çoğu zaman yeterliyken, düzinelerce rotası ve paylaşılan bileşen kütüphanesi olan bir uygulamada bu dört ayarın her biri gerçek bir fark yaratır. Değişikliği yapmadan önce mevcut build çıktısını kaydetmek, değişiklikten sonra aynı ölçümü tekrarlamak; bu karşılaştırma olmadan yapılan hiçbir yapılandırma değişikliği doğrulanmış sayılmaz.