Mert Özen Her Satırda Daha İleri
Frontend Build Araçları — #01

Build Aracı Neden Var? Module Graph, Transpiling, Bundling, Minification ve Dev Server'ın Görevi

Mert Özen 14 Kas 2026 15 dk 10 görüntülenme
Build Aracı Neden Var? Module Graph, Transpiling, Bundling, Minification ve Dev Server'ın Görevi

Build aracı, tarayıcının tek başına yapamadığı işi yapar: import'ları çözüp module graph'ı kurar, TypeScript'i çevirir, kullanılmayan kodu atar, dosyaları birleştirip küçültür ve içerikten hash'lenmiş adlar verir. Bookstore'un kitap arama widget'ında bundler'sız ESM'in 642 isteğini, Vite 8 ile 3 isteğe inen çıktıyı, Baseline build target'ını ve dev server ile production build farkını gerçek çıktılarla inceliyoruz.

Sürüm notu: Bu yazıdaki örnekler Vite 8.3 (Rolldown 1.2, Oxc, Lightning CSS 1.33), TypeScript 7.0, lodash-es 4.18 ve Node.js 24 ile denenmiştir; tarayıcı ölçümleri Playwright 1.65'in Chromium'uyla alınmıştır. Süre ölçümleri yazarın kendi makinesinden alındı; mutlak değerlere değil, aradaki orana bak.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) ASP.NET Core sayfalarına küçük bir "kitap bul" widget'ı eklemek istiyoruz: bir arama kutusu, yazdıkça süzülen bir kitap listesi. Arama her tuşta değil, kullanıcı yazmayı bırakınca çalışsın diye lodash-es'in debounce fonksiyonunu kullanıyoruz. İlk deneme hiçbir build aracı olmadan, tarayıcının kendi module sistemiyle yazıldı:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Book finder without a build step</title>
    <script type="importmap">
        {
            "imports": {
                "lodash-es": "/node_modules/lodash-es/lodash.js"
            }
        }
    </script>
    <script type="module" src="./main.js"></script>
</head>
<body>
    <input type="search" id="query" aria-label="Search books">
</body>
</html>
import { debounce } from "lodash-es";

const input = document.querySelector("#query");
const search = debounce(() => console.log(`searching for "${input.value}"`), 200);
input.addEventListener("input", search);
console.log("book finder ready");

Sayfa çalışıyor; konsolda book finder ready yazıyor, kutuya "dune" yazınca 200 ms sonra arama satırı geliyor. Soru şu: bu sayfa açılırken tarayıcı kaç HTTP isteği yapıyor? İki mi (HTML ve main.js), üç mü (bir de lodash), yoksa çok daha fazla mı? Bir sayı tahmin et ve aklında tut.

Bu yazı Build Araçları serisinin ilk yazısı. Serinin geri kalanında webpack, esbuild, Vite ve Angular CLI'ın build sistemini tek tek açacağız; burada hepsinin ortak işini, yani kaynak kodun tarayıcıya giden yolunu adım adım görüyoruz.

Script Etiketinden ESM'e: Kısa Bir Tarihçe

İlk dönemde JavaScript sayfaya sırayla eklenen script etiketleriydi. Her dosya global scope'a yazıyordu; jquery.js, onu kullanan cart.js'ten önce gelmezse sayfa bozuluyordu. Sıra bilgisi HTML'deydi, bağımlılık bilgisi hiçbir yerde yazmıyordu.

Bunun ilk çaresi IIFE (Immediately Invoked Function Expression; tanımlandığı anda çalışan fonksiyon) oldu: her dosya kendi fonksiyonunun içine sarılıp dışarıya tek bir global object açtı. Global çakışmalar azaldı ama sıra sorunu sürdü. 2009'da Node.js ile gelen CommonJS (require ve module.exports) bağımlılığı kodun içine yazdı; ancak senkron çalıştığı için tarayıcıda doğrudan kullanılamadı. Tarayıcı tarafında AMD (Asynchronous Module Definition; module'leri asenkron yükleyen tanım biçimi) ve RequireJS denendi. Browserify ve ardından webpack, CommonJS ile yazılmış kodu tarayıcı için tek dosyada birleştirerek bu iki dünyayı buluşturdu; bugünkü bundler'ların atası bunlar.

ES2015 ile dilin kendi module sistemi geldi: ESM (ECMAScript Modules; import ve export ile yazılan standart module sistemi). Statik import ifadeleri sayesinde bir dosyanın neye bağlı olduğu, kod çalışmadan önce okunabiliyor. Bu özellik, tree shaking dahil bugünkü build araçlarının yaptığı analizlerin çoğunun temeli. Tarayıcılar script type="module" desteğini 2017–2018 arasında verdi; import maps ise Mart 2023'te Safari 16.4 ile bütün büyük tarayıcılara ulaştı ve Eylül 2025'ten beri Baseline'da widely available. Yani tahmin sorusundaki sayfa, hiçbir build aracı olmadan bugün her güncel tarayıcıda çalışıyor. Module'lerin tarayıcıdaki davranışını ve import maps'in syntax'ını JavaScript Orta serisinin "JavaScript Module'leri Tarayıcıda: import, export, Dynamic import() ve Import Maps" yazısında anlatmıştık; burada maliyetine bakıyoruz.

Bundler'sız ESM Bugün Nerede Yeter?

Tahminin cevabı: 642 istek. Sayfayı Playwright'ın Chromium'uyla açıp her isteği saydık: 1 HTML, 1 main.js ve lodash-es'ten 640 dosya, toplam yaklaşık 615 KB sıkıştırılmamış JavaScript. Sebep, import { debounce } from "lodash-es" satırının lodash'ın barrel dosyasını, yani bütün fonksiyonları yeniden export eden lodash.js'i istemesi. Tarayıcı bu dosyayı okuyunca içindeki her import'u da istemek zorunda; hangi export'un kullanılacağını bilemez, çünkü bu bilgi ancak bütün graph'a bakınca ortaya çıkar.

Import map'i paketin klasörüne açıp yalnız ihtiyacımız olan dosyayı import edersek tablo değişiyor:

<script type="importmap">
    {
        "imports": {
            "lodash-es/": "/node_modules/lodash-es/"
        }
    }
</script>
import debounce from "lodash-es/debounce.js";

Bu sefer 16 istek ve 15 KB: debounce.js ve onun zincirleme import ettiği 13 küçük dosya. Aynı widget'ın bundle'lanmış hâli ise 3 istekle (HTML, bir CSS, bir JS) toplam 4.6 KB taşıyor. Ağ gecikmesi olmayan localhost'ta bile, bu makinede 642 istekli sayfanın load event'i 16 istekli olandan yaklaşık 12 kat geç geldi. Gerçek bir ağda her istek round-trip ve header maliyeti de ekler; HTTP/2'nin multiplexing'i bu farkı daraltır ama sıfırlamaz, çünkü tarayıcı bir module'ün import'larını ancak o module'ü indirip parse ettikten sonra öğrenir. Bu yüzden derin bir import zincirinde her indirme bir öncekini bekler.

Import map unutulursa ya da hiç yoksa sayfa tek satırda kırılır, çünkü tarayıcı lodash-es gibi çıplak bir paket adını kendisi çözemez:

TypeError: Failed to resolve module specifier "lodash-es". Relative references must start with either "/", "./", or "../".

Bundler'sız ESM; kod düz JavaScript, bağımlılıklar az ve sığ, sayfa da küçük ve iç kullanıma yönelikse (bir admin ekranı, bir prototip) yeter. TypeScript yazdığın anda yetmez, çünkü tarayıcı tip anotasyonlu kodu çalıştıramaz. Barrel dosyası olan bir paket kullandığında da yetmez; 642 istek bunun sonucu.

Bir Build'in Adımları: Resolve'dan Content Hash'e

Widget'ın gerçek hâli TypeScript ile yazıldı ve dört küçük module'den oluşuyor. Arama ve fiyat biçimlendirme ayrı dosyalarda, finder.ts hepsini birleştiriyor:

import { debounce } from "lodash-es";
import type { Book } from "./books";
import { formatPrice } from "./format";
import { searchBooks } from "./search";

export function mountBookFinder(host: HTMLElement, books: readonly Book[]): void {
    const input = document.createElement("input");
    input.type = "search";
    input.className = "finder-input";
    input.placeholder = "Search by title or author";
    input.setAttribute("aria-label", "Search books");

    const list = document.createElement("ul");
    list.className = "finder-results";

    const render = (items: Book[]): void => {
        list.replaceChildren(...items.map(book => {
            const item = document.createElement("li");
            item.textContent = `${book.title} · ${book.author} · ${formatPrice(book.price)}`;
            return item;
        }));
    };

    input.addEventListener("input", debounce(() => render(searchBooks(books, input.value)), 200));
    host.replaceChildren(input, list);
    render([...books]);
}
const priceFormat = new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY" });

export function formatPrice(value: number): string {
    return priceFormat.format(value);
}

export function formatIsbn(isbn: string): string {
    return `${isbn.slice(0, 3)}-${isbn.slice(3, 4)}-${isbn.slice(4, 7)}-${isbn.slice(7, 12)}-${isbn.slice(12)}`;
}

main.ts CSS'i import edip widget'ı #book-finder element'ine bağlıyor; books.ts örnek kitap listesini tutuyor (Dune, Emma, The Hobbit, Middlemarch, Pride and Prejudice). Bu kodu Vite 8 ile build ediyoruz. Vite burada yalnız taşıyıcı; adımlar webpack'te, esbuild'de ve Angular CLI'da da aynı. Bundler'ın içini görmek için küçük bir plugin yazdık: build bitmeden önce module graph'ı ve chunk'a giren module'leri yazdırıyor. Plugin yalnız graph mode'unda devreye giriyor, normal build'i etkilemiyor:

import { defineConfig, normalizePath, type Plugin } from "vite";

// "npx vite build --mode graph" ile çalışır: module graph'ı ve her chunk'a gireni yazar
function moduleGraphReport(): Plugin {
    return {
        name: "module-graph-report",
        apply: "build",
        generateBundle(_options, bundle) {
            const root = normalizePath(process.cwd()) + "/";
            const short = (id: string) => id.replace(root, "");
            const graph = [...this.getModuleIds()];
            const fromLodash = graph.filter(id => id.includes("/lodash-es/")).length;
            console.log(`\nmodule graph: ${graph.length} modules (${fromLodash} from lodash-es)`);
            for (const id of graph.filter(id => id.includes("/src/"))) {
                const imports = this.getModuleInfo(id)?.importedIds ?? [];
                console.log(`  ${short(id)} -> ${imports.map(short).join(", ") || "(none)"}`);
            }
            for (const output of Object.values(bundle)) {
                if (output.type !== "chunk") {
                    continue;
                }
                const rendered = Object.entries(output.modules).filter(([, m]) => m.renderedLength > 0);
                const lodash = rendered.filter(([id]) => id.includes("/lodash-es/")).length;
                console.log(`chunk ${output.fileName}: ${rendered.length} modules rendered (${lodash} from lodash-es)`);
            }
        },
    };
}

export default defineConfig(({ mode }) => ({
    plugins: mode === "graph" ? [moduleGraphReport()] : [],
}));
npx vite build --mode graph
vite v8.3.8 building client environment for graph...
transforming...
✓ 649 modules transformed.
rendering chunks...

module graph: 648 modules (640 from lodash-es)
  src/styles.css -> (none)
  src/books.ts -> (none)
  src/search.ts -> (none)
  src/finder.ts -> node_modules/lodash-es/lodash.js, src/format.ts, src/search.ts
  src/format.ts -> (none)
  src/main.ts -> src/styles.css, src/books.ts, src/finder.ts
chunk assets/index-B8efG9ka.js: 20 modules rendered (14 from lodash-es)
computing gzip size...
dist/index.html                 0.39 kB │ gzip: 0.26 kB
dist/assets/index-DDpRcdOH.css  0.30 kB │ gzip: 0.19 kB
dist/assets/index-B8efG9ka.js   4.04 kB │ gzip: 1.90 kB

✓ built in 38ms

Bu çıktı bir build'in adımlarını tek tek gösteriyor:

01-build-pipeline-tr.png

Resolve: Bundler entry'den (burada index.html) başlar ve her import'un hangi dosyaya karşılık geldiğini bulur. "lodash-es" çıplak adı node_modules/lodash-es/package.json üzerinden lodash.js'e, "./format" uzantısız yolu src/format.ts'e çözüldü. Tarayıcının import map ile yaptığı işi bundler build sırasında, Node.js'in çözümleme kurallarıyla yapıyor.

Module graph: Çözülen her dosya graph'a bir düğüm olarak girer, import'lar kenar olur. Graph 648 module; 640'ı lodash-es'ten. Dikkat: search.ts Book tipini import ettiği hâlde graph'ta kenarı yok. import type transform sırasında tamamen silindiği için bundler o bağımlılığı hiç görmüyor.

Transform: Her dosya tarayıcının anlayacağı JavaScript'e çevrilir. Vite 8'de TypeScript'i Oxc (Rust ile yazılmış parser, transformer ve minifier ailesi) çeviriyor, CSS'i Lightning CSS işliyor. Sass, JSX (JavaScript içine HTML benzeri syntax yazmayı sağlayan uzantı) ya da Vue dosyası olsaydı onların transform'u da bu adımda çalışırdı.

Tree shaking ve chunking: Graph'ın tamamı çıktıya girmez. Bundler hangi export'un gerçekten kullanıldığına bakar; 640 lodash module'ünden yalnız 14'ü, yani debounce ve onun ihtiyaç duyduğu yardımcılar chunk'a girdi. Code splitting olmadığı için her şey tek chunk'ta toplandı.

Minify ve content hash: Chunk küçültülür ve dosya adına içerikten üretilen bir hash eklenir: index-B8efG9ka.js. Assets adımında da CSS ve görseller aynı şekilde adlandırılır; index.html yeni adlarla yeniden yazılır.

Tree Shaking, Minification ve Content Hash'i Çıktıda Görmek

Minify kapalı bir build, tree shaking'in ve scope hoisting'in sonucunu okunur biçimde gösterir:

npx vite build --minify false
// node_modules/lodash-es/now.js
...
// node_modules/lodash-es/debounce.js
...
// src/format.ts
var priceFormat = new Intl.NumberFormat("tr-TR", {
	style: "currency",
	currency: "TRY"
});
function formatPrice(value) {
	return priceFormat.format(value);
}
// src/search.ts

İki şey görülüyor. Birincisi, formatIsbn yok; export edilmiş ama kimse import etmediği için çıktıya alınmadı. İkincisi, module'ler ayrı fonksiyonlara sarılmadı; hepsi tek bir scope'ta, yorum satırlarıyla ayrılmış hâlde art arda duruyor. Buna scope hoisting deniyor ve ESM'in statik yapısı sayesinde mümkün. CommonJS'te require çalışma zamanında çağrılan bir fonksiyon olduğu için bundler aynı kesinlikte karar veremez; bu konuya serinin tree shaking yazısında döneceğiz.

Boyut farkı minification'ın payını gösteriyor: minify kapalıyken JS 15.30 kB (gzip 4.90 kB), açıkken 4.04 kB (gzip 1.90 kB). Minifier boşlukları atmakla kalmaz; yerel değişken adlarını kısaltır, true yerine !0 yazar, hiç çalışmayacak dalları siler.

Content hash'in davranışını görmek için aynı build'i değişiklik yaparak tekrar tekrar çalıştırdık:

DeğişiklikCSS dosyasıJS dosyası
yok (iki kez build)index-DDpRcdOH.cssindex-B8efG9ka.js
styles.css'te bir renkindex-DqKZ1iiT.cssindex-CvX385Gp.js
geri alındıindex-DDpRcdOH.cssindex-B8efG9ka.js
finder.ts'te placeholder metniindex-DDpRcdOH.cssindex-UvNSaxts.js

Aynı girdi her seferinde aynı adı veriyor; build deterministik. Bu sayede sunucu bu dosyaları bir yıllık Cache-Control: immutable ile gönderebilir; içerik değişince ad da değişeceği için tarayıcı eski dosyayı kullanmaz. İlginç olan ikinci satır: yalnız CSS'i değiştirdik ama JS'in hash'i de değişti. Vite'ın CSS plugin'i, bir JS chunk'ının hash'ine o chunk'ın import ettiği CSS dosyalarının adlarını ekliyor. Ters yönde böyle bir bağ yok: JS değişince CSS'in adı aynı kaldı.

Transpiler, Bundler, Minifier, Dev Server: Kim Ne Yapıyor?

"Build aracı" dediğimiz şey aslında birkaç ayrı rolün bir araya gelmesi. Hangi aracın hangi rolü üstlendiğini bilmek, bir sorun çıktığında nereye bakacağını söyler:

RolNe yapar2026'da tipik araçlar
Type checkerTipleri doğrular, dosya üretmesi gerekmeztsc (TypeScript 7, Go ile yazılan derleyici)
TranspilerTS ve JSX'i JavaScript'e çevirir, yeni syntax'ı hedefe indirirOxc, esbuild, SWC (Rust ile yazılmış bir transpiler), Babel
BundlerResolve, module graph, tree shaking, chunkRolldown, Rollup, webpack, Rspack, esbuild
MinifierBoşluk, ad kısaltma, ölü kod silmeOxc minifier, esbuild, Terser
CSS işleyiciNesting'i indirme, prefix, CSS minifyLightning CSS, PostCSS
Dev serverDosyaları istek geldikçe çevirip sunar, HMR (sayfayı yenilemeden yalnız değişen module'ü değiştirme)Vite, webpack-dev-server, Angular CLI

En sık karıştırılan ayrım type checker ile transpiler arasında. Vite, esbuild ve webpack'in yeni yerleşik TypeScript desteği tipleri yalnızca siler, kontrol etmez. books.ts'te bir fiyatı yanlışlıkla string yazdık ve iki aracı da çalıştırdık:

npx vite build
# ...
# ✓ built in 28ms

npx tsc --noEmit
# src/books.ts(9,70): error TS2322: Type 'string' is not assignable to type 'number'.

Build başarıyla bitti ve hatalı kodu üretti; hatayı yalnız tsc yakaladı. Bu yüzden package.json'daki typecheck script'i (tsc --noEmit) CI'da build'den ayrı bir adım olarak çalışmalı. TypeScript 7'nin Go ile yazılan derleyicisi bu adımı eskisine göre çok hızlandırdı; ayrıntıları TypeScript İleri serisinin "TypeScript 7 ve Sonrası: Go ile Yazılan Derleyici ve Yeni Varsayılanlar" yazısında.

Bugünkü araçların çoğu bu rollerin birkaçını birden üstleniyor. esbuild transpiler, bundler ve minifier'dır; Vite 8 dev server'dır ve production build'de Rolldown'u (bundler), Oxc'yi (transpiler ve minifier) ve Lightning CSS'i bir arada çalıştırır. Angular CLI ise kendi builder'ı içinde esbuild ve Vite'ı kullanır. Bu araçları serinin sonraki yazılarında tek tek açacağız.

Build Target ve Baseline

Transpiler'ın ikinci görevi, yazdığın syntax'ı desteklemek istediğin en eski tarayıcının anlayacağı biçime indirmek. Bu hedefe build target deniyor. Vite 8'in varsayılan build.target değeri 'baseline-widely-available': 1 Ocak 2026 itibarıyla Baseline'da widely available olan özellikleri destekleyen en düşük tarayıcı sürümleri, yani chrome111, edge111, firefox114, safari16.4 ve ios16.4. Baseline (web-features projesinin bir özelliğin bütün büyük tarayıcılarda ne zamandan beri desteklendiğini gösteren etiketi), "widely available" için 30 ay kuralını kullanıyor.

Aynı widget'ı üç farklı target ile build ettik. JavaScript tarafında fark lodash'tan gelen tek bir satırda çıktı:

npx vite build --target esnext
# ...}catch{}var i=s.call(e)...

npx vite build --target es2015
# ...}catch(e){}var i=s.call(e)...

Parametresiz catch ES2019 ile geldi; ES2015 hedefinde transpiler ona kullanılmayan bir parametre ekledi. CSS tarafında fark daha görünür. Kaynakta iç içe yazdığımız kurallar, varsayılan target'ta düzleştirildi:

#book-finder{max-width:32rem;font-family:system-ui,sans-serif}#book-finder .finder-input{...}#book-finder .finder-results li{...}

esnext hedefinde nesting olduğu gibi kaldı (#book-finder{...;& .finder-input{...}}), ES2015 hedefinde ise Lightning CSS system-ui için eski tarayıcılara -apple-system, Segoe UI, Roboto gibi yedek fontlar ekledi ve dosya 0.30 kB'tan 0.39 kB'a çıktı. Burada küçük bir ayrıntı var: CSS nesting Haziran 2026'da widely available oldu, ama Vite'ın varsayılanı 2026-01-01 tarihine sabitlendiği için hâlâ düzleştiriliyor. Target bir tarih ve sürüm listesi; araç sürümünle birlikte ne zaman ilerlediğini sürüm notlarından takip etmek gerekiyor.

Dev Build ve Production Build

Şimdiye kadar baktığımız her şey production build'di. Geliştirme sırasında hedef farklı: dosyayı kaydettiğin anda sonucu görmek. Vite'ın dev server'ında aynı widget'ı açıp istekleri saydık:

npx vite
/
/src/main.ts
/src/styles.css
/src/books.ts
/src/finder.ts
/src/format.ts
/src/search.ts
/node_modules/.vite/deps/lodash-es.js?v=b6e28c33
/@vite/client
/node_modules/vite/dist/client/env.mjs

Dev server bundle yapmıyor; her kaynak dosya ayrı bir istek olarak, istendiği anda çevrilip native ESM olarak sunuluyor. Bu yüzden proje büyüse de açılış süresi neredeyse sabit kalıyor. Ama lodash-es 640 dosya olarak gelmiyor: Vite açılışta bağımlılıkları bir kez önceden bundle'layıp node_modules/.vite/deps/lodash-es.js adında tek dosyaya koymuş. Bu, ilk bölümdeki 642 istek sorununun dev tarafındaki çözümü; adı dependency pre-bundling ve serinin Vite yazısında ayrıntısıyla göreceğiz. Bu sayfa 10 istekte yaklaşık 1.8 MB taşıdı. Bunun 1.5 MB'ı, lodash'ın tamamını içeren ve source map'i dosyanın içine gömülü pre-bundle dosyası; 220 KB kadarı da Vite'ın HMR için sayfaya eklediği client. Hiçbir şey minify edilmiyor, tree shaking yapılmıyor. Geliştirme için doğru bir tercih, production için değil.

Dev ve production build'in farkı kısaca şöyle: dev build hızlı geri bildirim, okunur kod, source map ve HMR için optimize edilir. Production build ise az istek, küçük dosya, tree shaking, minification ve uzun süre cache'lenebilen hash'li adlar için. İkisi farklı yollardan geçtiği için "dev'de çalışıyor, production'da çalışmıyor" hataları da buradan çıkar.

Üretimde Dikkat

Build çıktısını CI'da ölç. Vite'ın her build'de yazdığı boyut tablosu (4.04 kB │ gzip: 1.90 kB) en ucuz alarm sistemi. Bir pull request'te bu sayı birden ikiye katlandıysa, biri büyük bir bağımlılık eklemiştir. Bundle'ı neyin şişirdiğini bulmayı serinin bundle analizi yazısında, küçülen bundle'ın sayfa hızına etkisini ölçmeyi ise ilerideki Web Performansı serisinde ele alacağız.

Hash'li dosyalarla cache stratejisi iki parçalı: assets/ altındaki hash'li dosyalar uzun süre ve immutable ile cache'lenir, onlara referans veren index.html ise hiç cache'lenmez ya da kısa süre cache'lenir. HTML eski kalırsa yeni deploy'dan sonra kullanıcı eski hash'leri ister; eski dosyaları sunucudan sildiysen 404 alır. Bu yüzden deploy'da bir önceki sürümün asset'lerini bir süre daha tutmak iyi bir alışkanlık.

Build'i tekrarlanabilir tut. Aynı commit'ten iki build aynı hash'leri vermeli; vermiyorsa ya kilit dosyası (package-lock.json) commit'lenmemiştir ya da build bir tarih veya ortam değişkeni gömüyordur. CI'da npm install yerine npm ci kullan. Type check de aynı pipeline'da build'den ayrı ama zorunlu bir adım olmalı; bundler tip hatasını görmez.

Sık Düşülen Tuzaklar

Birincisi, bundler'ı yalnız "dosyaları birleştiren araç" sanmak. Bugün dosyayı birleştirmek işin en küçük parçası. Asıl kazanç graph analizinden, yani tree shaking'den, scope hoisting'den ve chunk'ların akıllıca bölünmesinden geliyor.

İkincisi, barrel import'un bedelini görmemek. import { debounce } from "lodash-es" bundle'da sorun değil, çünkü bundler graph'ın tamamını görüp gereksizi atıyor. Ama aynı satır bundler'sız ESM'de 640 istek, dev server'da da pre-bundling olmasa yüzlerce istek demek. Kendi kodundaki index.ts barrel'ları da aynı etkiyi yapar.

Üçüncüsü, build target'ı varsayılan bırakıp "neden eski Safari'de çalışmıyor" ya da tersine "neden çıktı bu kadar büyük" diye şaşırmak. Target'ı desteklediğin tarayıcı listesine göre bilinçli seç; çok eski bir target gereksiz polyfill ve dönüşümle bundle'ı büyütür.

Dördüncüsü, dist/ klasörünü commit'lemek ya da elle düzeltmek. Çıktı her build'de yeniden üretilir; değişiklik kaynakta ya da config'te yapılır.

Küçük Bir Deneme

★☆☆ nobuild/index.html'i npx http-server . -p 8080 -c-1 ile sun ve tarayıcıda DevTools'un Network sekmesini açarak istek sayısını kendin gör. Sonra import map'i "lodash-es/" biçimine çevirip main.js'te lodash-es/debounce.js'i import et ve sayıyı karşılaştır.

★★☆ format.ts'teki formatIsbn'i finder.ts'te kullan; listede her kitabın ISBN'ini (uluslararası standart kitap numarası) de göster. npx vite build --mode graph çıktısında nelerin değiştiğini, JS dosyasının boyutunu ve hash'ini önceki build'le karşılaştır.

★★★ vite.config.ts'e build: { cssTarget: "chrome120" } ekle ve CSS çıktısında nesting'in korunup korunmadığına bak. Ardından build.target'ı kendi projenin desteklediği tarayıcı listesine göre belirle ve seçimini bir README satırıyla gerekçelendir.

Bir sonraki yazıda bu adımların en eski ve hâlâ en yaygın uygulayıcısına geçiyoruz: webpack. Aynı widget'ı webpack 5.112 ile build edecek, loader zincirinin sırasını, plugin hook'larını ve webpack'in son sürümlerde core'a aldığı native CSS ve HTML desteğini göreceğiz.