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

Vite: Native ESM Dev Server, HMR, Dependency Pre-bundling ve Rolldown ile Production Build

Mert Özen 14 Kas 2026 14 dk 14 görüntülenme
Vite: Native ESM Dev Server, HMR, Dependency Pre-bundling ve Rolldown ile Production Build

Vite geliştirmede kaynak kodu bundle'lamaz: her dosyayı istendiği anda çevirip native ESM olarak sunar, bağımlılıkları bir kez pre-bundle eder; production'da Vite 8 ile Rolldown, Oxc ve Lightning CSS kullanır. Bookstore widget'ını ASP.NET Core'a bağlayıp /api proxy'sini, HMR'ın ne zaman sayfayı yenilediğini, manifest.json'u okuyan bir Razor tag helper'ı, VITE_ değişkenlerinin bundle'a gömülmesini ve Vite 7'den geçişte sessizce etkisiz kalan ayarları görüyoruz.

Sürüm notu: Bu yazıdaki örnekler Vite 8.3 (Rolldown 1.2, Oxc, Lightning CSS 1.33), TypeScript 7.0, Node.js 24 ve .NET 10 (ASP.NET Core 10) ile denenmiştir; tarayıcı davranışı Playwright 1.65'in Chromium'uyla gözlenmiştir. Süreler yazarın kendi makinesinden alındı.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) server-side render edilen Bookstore.Web sitesine kitap arama widget'ını yerleştirdik. Razor sayfası http://localhost:5080'de çalışıyor, widget'ın TypeScript module'leri ise Vite'ın dev server'ından (http://localhost:5173) geliyor. Sayfayı açıp arama kutusuna "eliot" yazdık; liste Middlemarch ve Silas Marner'a indi. Sayfa açıkken iki değişiklik yapıyoruz:

/* 1) styles.css: liste satırlarının alt çizgisi */
border-bottom: 2px solid #f59e0b;

// 2) finder.ts: arama kutusunun placeholder metni
input.placeholder = "Search by title, author";

Her değişiklikten sonra tarayıcıda ne olur? Sayfa yenilenir mi, arama kutusundaki "eliot" yerinde kalır mı? İki değişiklik için aynı cevabı mı veriyorsun?

Bu yazı Build Araçları serisinin dördüncü yazısı. Serinin ilk yazısında Vite'ı yalnız bir build aracı olarak kullandık; webpack ve esbuild yazılarından sonra şimdi Vite'ın kendisine bakıyoruz: native ESM (ECMAScript Modules; tarayıcının import/export ile çalışan kendi module sistemi) dev server, dependency pre-bundling, HMR (Hot Module Replacement; sayfayı yenilemeden yalnız değişen module'ü değiştirme), ASP.NET Core ile birlikte geliştirme ve Vite 8 ile Rolldown'a geçen production build.

Dev Server: Bundle Yok, Native ESM Var

Vite'ın dev server'ı kaynak kodu bundle'lamaz. Tarayıcı bir module istediğinde Vite o dosyayı o anda çevirir (TypeScript'i Oxc ile JavaScript'e, CSS'i bir JS module'üne) ve native ESM olarak döndürür. Razor sayfasının açılışında Chromium'un yaptığı istekler:

http://localhost:5080/
http://localhost:5173/@vite/client
http://localhost:5173/src/main.ts
http://localhost:5173/src/styles.css
http://localhost:5173/src/api.ts
http://localhost:5173/src/books.ts
http://localhost:5173/src/finder.ts
http://localhost:5173/node_modules/vite/dist/client/env.mjs
http://localhost:5173/node_modules/.vite/deps/lodash-es.js
http://localhost:5173/src/format.ts
http://localhost:5173/src/search.ts
http://localhost:5080/api/books

HTML ve API ASP.NET Core'dan, module'ler Vite'tan geliyor. Her kaynak dosya ayrı bir istek; finder.ts'i değiştirirsen yalnız o dosya yeniden çevrilir. Bu yüzden Vite'ın açılış süresi projenin büyüklüğünden neredeyse bağımsız: bu makinede dev server 55–125 ms arasında hazır oldu ve hiçbir dosyaya o ana kadar dokunmamıştı. İş, sayfa istendiğinde ve yalnız istenen dosyalar için yapılıyor. Production build ise bambaşka bir yol izliyor:

04-dev-vs-build-tr.png

Dependency Pre-bundling

Listede bir istisna var: lodash-es, 640 dosya yerine tek bir node_modules/.vite/deps/lodash-es.js olarak geliyor. Serinin ilk yazısında aynı import'un bundler'sız sayfada 642 istek ürettiğini görmüştük. Vite bunu dependency pre-bundling ile çözüyor: açılışta kaynak kodu tarayıp kullanılan paketleri bulur ve her birini bir kez, Vite 8'de Rolldown ile, tek dosyaya bundle'lar. .vite cache'ini silip --debug deps ile başlattığımızda süreç şöyle görünüyor (yollar kısaltıldı):

npx vite --debug deps
vite:deps scanning for dependencies...
vite:deps Crawling dependencies using entries:
  .../widget/api.html
  .../widget/index.html
  .../widget/federation/host.html
  .../widget/nobuild/index.html
vite:deps Scan completed in 6.32ms:
  lodash-es -> .../widget/node_modules/lodash-es/lodash.js
vite:deps Dependencies bundled in 32.82ms
vite:deps dependencies optimized

Pre-bundling'in iki amacı var. Birincisi istek sayısı: yüzlerce küçük dosyalı bir paket tek dosyaya iniyor. İkincisi uyumluluk: CommonJS ile yazılmış paketler tarayıcıda doğrudan çalışmaz; pre-bundling onları ESM'e çeviriyor. Sonuç node_modules/.vite/deps/_metadata.json'a yazılıyor:

{
  "hash": "94cf188b",
  "configHash": "c9948d63",
  "lockfileHash": "7c0d8d5e",
  "browserHash": "7cf20e02",
  "optimized": {
    "lodash-es": {
      "src": "../../lodash-es/lodash.js",
      "file": "lodash-es.js",
      "fileHash": "9324b7f4",
      "needsInterop": false
    }
  },
  "chunks": {}
}

lockfileHash ve configHash alanları cache'in ne zaman geçersiz sayılacağını belirliyor: package-lock.json ya da vite.config.ts değişince bir sonraki açılışta pre-bundling yeniden yapılıyor; değişmediyse log Hash is consistent. Skipping. diyor. browserHash, tarayıcıya giden URL'deki ?v=7cf20e02 parametresi; pre-bundle değişince URL de değiştiği için tarayıcı eski dosyayı cache'ten kullanmıyor. Elle bir paket dosyasını düzenlediğinde (örneğin node_modules içinde hata ayıklarken) Vite bunu fark etmez; npx vite --force ile yeniden bundle'latırsın ve log'da Forced re-optimization of dependencies satırını görürsün.

Taramanın giriş listesine dikkat: Vite kök klasördeki bütün HTML dosyalarını tarıyor, webpack yazısındaki federation test sayfası ve ilk yazıdaki bundler'sız sayfa dahil. Büyük bir projede bu gereksiz tarama demek; optimizeDeps.entries ile taranacak dosyaları sınırlayabilirsin.

HMR: Ne Zaman Yerinde Güncelleme, Ne Zaman Yeniden Yükleme?

Tahminin cevabı iki değişiklik için farklı. Sayfayı Playwright ile açık tutup değişiklikleri yaptık ve sayfanın load event'lerini saydık:

console: debug [vite] hot updated: /src/styles.css
css change -> loads: 1 input: eliot border: 2px
console: debug [vite] connecting...
console: debug [vite] connected.
ts change -> loads: 2 input: "" placeholder: Search by title, author

Vite'ın terminalindeki karşılığı:

3:52:02 PM [vite] (client) hmr update /src/styles.css
3:52:05 PM [vite] (client) page reload src/finder.ts

CSS değişikliğinde sayfa yenilenmedi; yeni kural uygulandı (border: 2px) ve kutudaki "eliot" yerinde kaldı. finder.ts değişikliğinde ise sayfa baştan yüklendi ve yazdığımız metin kayboldu. Sebep HMR'ın sınırlarla (boundary) çalışması. Bir module değişince Vite, import zincirinde yukarı doğru o güncellemeyi "kabul eden" bir module arar. CSS module'leri kendi güncellemelerini kendileri kabul eder: eski style etiketinin içeriği değiştirilir, o kadar. finder.ts'i ise kimse kabul etmiyor; zincir main.ts'e, oradan da sayfanın kendisine ulaşıyor ve Vite güvenli olan tek şeyi yapıyor: sayfayı yeniden yüklemek.

Bir module, import.meta.hot.accept() ile kendi güncellemesini kabul edebilir; ama o zaman eski module'ün kurduğu event listener'ları ve DOM'u (sayfanın tarayıcıdaki element ağacı) temizlemek de senin işin. React (Fast Refresh), Vue ve Svelte'in Vite plugin'leri bu sınırları her component için otomatik kurar; component'in state'i korunarak yalnız değişen component yeniden çizilir. Bizim widget'ımız framework kullanmayan düz TypeScript olduğu için CSS dışındaki her değişiklik tam yeniden yükleme demek. Bu kötü bir şey değil; yeniden yükleme, Vite'ın değiştirdiği tek dosyayı yeniden istemesinden ibaret ve yine milisaniyeler sürüyor. Angular'ın kendi dev server'ındaki component ve stil HMR'ını serinin Angular CLI yazısında göreceğiz.

Bookstore.Web ile Geliştirme: Proxy ve Dev Server Script'leri

Widget'ı iki şekilde geliştiriyoruz: ya Vite'ın kendi sayfasında tek başına ya da ASP.NET Core'un Razor sayfasının içinde. İkisi için de Vite config'ine şu server bölümünü ekledik:

server: {
    port: 5173,
    strictPort: true,
    // Bookstore.Web (ASP.NET Core) 5080'de: /api oraya gider, onun sayfaları bizim module'lerimizi yükler
    proxy: { "/api/": "http://localhost:5080" },
    cors: { origin: "http://localhost:5080" },
},

proxy, Vite'ın kendi sayfasından yapılan /api/ isteklerini ASP.NET Core'a iletiyor. Widget artık kitapları, host element'teki data-source attribute'unda bir adres varsa API'den çekiyor, yoksa önceki yazılardaki örnek listeyi kullanıyor:

import "./styles.css";
import { fetchBooks } from "./api";
import { sampleBooks, type Book } from "./books";
import { mountBookFinder } from "./finder";

async function start(host: HTMLElement): Promise<void> {
    const source = host.dataset.source;
    const books: readonly Book[] = source ? await fetchBooks(source) : sampleBooks;
    mountBookFinder(host, books);
}

const host = document.querySelector<HTMLElement>("#book-finder");
if (host) {
    void start(host);
}

Bu sayede index.html önceki yazılardaki gibi örnek veriyle çalışmaya, webpack ve esbuild build'leri de değişmeden derlenmeye devam ediyor. Yeni api.html sayfası ise <div id="book-finder" data-source="/api/books"> ile API'yi kullanıyor. API, Bookstore.Web'in içindeki tek satırlık bir minimal API endpoint'i: app.MapGet("/api/books", (BookCatalog catalog) => catalog.GetAll());.

İlk denemede proxy anahtarı "/api" idi ve http://localhost:5173/api.html 404 verdi. Vite'ın proxy anahtarı bir önek eşleşmesi; /api önekiyle başlayan /api.html isteği de ASP.NET Core'a gitmiş ve orada bulunamamıştı. Anahtarı "/api/" yapınca sayfa, 7 kitaplık API listesiyle açıldı ve "austen" araması üç kitap döndürdü.

İkinci yol, Razor sayfasının script'leri doğrudan Vite'tan yüklemesi. Bunun için Vite'ın backend integration dokümanındaki iki script etiketini Development ortamında sayfaya yazmak yeterli: önce HMR istemcisi (/@vite/client), sonra entry. Bunu bir tag helper'a verdik; dev server adresi yalnız appsettings.Development.json'da duruyor:

{
  "Vite": {
    "DevServerUrl": "http://localhost:5173"
  }
}
dotnet run --project server/Bookstore.Web --launch-profile vite-dev
npm run dev

Razor'ın ürettiği head:

<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/src/main.ts"></script>

Sayfa 5080'den, module'ler 5173'ten geldiği için bu farklı origin'den bir module yüklemesi; tarayıcı CORS (Cross-Origin Resource Sharing; bir sitenin başka bir origin'den kaynak yüklemesine izin veren mekanizma) kontrolü yapar. Vite'ın varsayılan CORS ayarı yalnız localhost origin'lerine izin veriyor; cors.origin'i açıkça yazmak, backend farklı bir host adıyla açıldığında da çalışmasını garanti ediyor. Önce tahmin sorusunda gördüğümüz HMR bu düzende çalışıyor: CSS değişikliği ASP.NET Core sayfasında yerinde uygulanıyor.

Production: manifest.json'dan Razor Layout'a

Production'da dev server yok; Razor'ın hash'li dosya adlarını bilmesi gerekiyor. Vite bunun için build.manifest'i sunuyor. Config'e yalnız server mode'unda devreye giren bir bölüm ekledik:

...(mode === "server" && {
    base: "/dist/",
    input: "src/main.ts",
    build: {
        manifest: true,
        outDir: "../server/Bookstore.Web/wwwroot/dist",
        emptyOutDir: true,
        modulePreload: { polyfill: false },
    },
}),

input, entry'yi index.html yerine doğrudan main.ts yapıyor; Vite 8'in dokümanı bunu build.rolldownOptions.input yerine üst seviyedeki input option'ıyla yazmayı öneriyor. base: "/dist/", çıktının içinde üretilen URL'lerin (örneğin CSS'teki bir url() ya da dinamik import edilen bir chunk) site kökünde değil /dist/ altında aranmasını sağlıyor. modulePreload.polyfill'i kapattık, çünkü HTML entry olmadığında Vite polyfill'i sayfaya ekleyemiyor; doküman bu durumda entry'nin başına import "vite/modulepreload-polyfill" yazmayı öneriyor, ama bu import'u webpack ve esbuild tanımıyor. Widget'ta dinamik import olmadığı için polyfill'e de ihtiyaç yok.

npm run build:server
../server/Bookstore.Web/wwwroot/dist/.vite/manifest.json       0.18 kB │ gzip: 0.14 kB
../server/Bookstore.Web/wwwroot/dist/assets/main-DDpRcdOH.css  0.30 kB │ gzip: 0.19 kB
../server/Bookstore.Web/wwwroot/dist/assets/main-DH5dtSTh.js   3.59 kB │ gzip: 1.74 kB

✓ built in 64ms
{
  "src/main.ts": {
    "file": "assets/main-DH5dtSTh.js",
    "name": "main",
    "src": "src/main.ts",
    "isEntry": true,
    "css": [
      "assets/main-DDpRcdOH.css"
    ]
  }
}

Manifest'in anahtarı entry'nin kaynak yolu; file çıktı dosyası, css o entry'nin CSS dosyaları. Ortak chunk'lar olsaydı imports alanında, dinamik import'lar dynamicImports alanında listelenirdi. ASP.NET Core tarafında manifest'i bir kez okuyan bir service yazdık:

using System.Text.Json;
using Microsoft.Extensions.Options;

namespace Bookstore.Web.Vite;

public sealed record ViteChunk(string File, string? Src, bool IsEntry, IReadOnlyList<string>? Css, IReadOnlyList<string>? Imports);

public sealed class ViteManifest(IWebHostEnvironment environment, IOptions<ViteOptions> options)
{
    private readonly Lazy<IReadOnlyDictionary<string, ViteChunk>> chunks = new(() =>
    {
        string path = Path.Combine(environment.WebRootPath, options.Value.OutputPath, ".vite", "manifest.json");
        using FileStream stream = File.OpenRead(path);
        return JsonSerializer.Deserialize<Dictionary<string, ViteChunk>>(stream, JsonSerializerOptions.Web)
            ?? throw new InvalidOperationException($"Empty Vite manifest: {path}");
    });

    public ViteChunk GetEntry(string src) =>
        chunks.Value.TryGetValue(src, out ViteChunk? chunk) && chunk.IsEntry
            ? chunk
            : throw new InvalidOperationException($"'{src}' is not an entry in the Vite manifest. Did you run 'npm run build:server'?");

    // Entry'nin statik import ettiği chunk'lar, önce bağımlılıklar (Vite dokümanındaki dolaşmayla aynı)
    public IReadOnlyList<ViteChunk> GetImportedChunks(ViteChunk entry)
    {
        List<ViteChunk> result = [];
        HashSet<string> seen = [];
        Visit(entry);
        return result;

        void Visit(ViteChunk chunk)
        {
            foreach (string key in chunk.Imports ?? [])
            {
                if (seen.Add(key))
                {
                    ViteChunk imported = chunks.Value[key];
                    Visit(imported);
                    result.Add(imported);
                }
            }
        }
    }
}

Etiketleri yazan tag helper, Vite dokümanının önerdiği sırayı izliyor: önce entry'nin ve import ettiği chunk'ların CSS'leri, sonra entry'nin script'i, en son import edilen chunk'lar için modulepreload:

using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Options;

namespace Bookstore.Web.Vite;

// <vite-entry src="src/main.ts" />: Development'ta dev server script'leri, diğer ortamlarda manifest'teki hash'li dosyalar
[HtmlTargetElement("vite-entry", TagStructure = TagStructure.WithoutEndTag)]
public sealed class ViteEntryTagHelper(ViteManifest manifest, IOptions<ViteOptions> options, HtmlEncoder encoder) : TagHelper
{
    public required string Src { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = null;
        string? devServer = options.Value.DevServerUrl?.TrimEnd('/');

        if (!string.IsNullOrEmpty(devServer))
        {
            output.Content.AppendHtml(Script($"{devServer}/@vite/client"));
            output.Content.AppendHtml(Script($"{devServer}/{Src}"));
            return;
        }

        ViteChunk entry = manifest.GetEntry(Src);
        IReadOnlyList<ViteChunk> imported = manifest.GetImportedChunks(entry);
        string basePath = $"/{options.Value.OutputPath}/";

        foreach (string css in imported.SelectMany(chunk => chunk.Css ?? []).Concat(entry.Css ?? []).Distinct())
        {
            output.Content.AppendHtml($"<link rel=\"stylesheet\" href=\"{encoder.Encode(basePath + css)}\">\n");
        }
        output.Content.AppendHtml(Script(basePath + entry.File));
        foreach (ViteChunk chunk in imported)
        {
            output.Content.AppendHtml($"<link rel=\"modulepreload\" href=\"{encoder.Encode(basePath + chunk.File)}\">\n");
        }
    }

    private string Script(string src) => $"<script type=\"module\" src=\"{encoder.Encode(src)}\"></script>\n";
}

Layout'ta kullanımı tek satır: <vite-entry src="src/main.ts" />. Uygulamayı vite-manifest profiliyle (Production ortamı, dev server adresi yok) başlattığımızda Razor'ın ürettiği head:

dotnet run --project server/Bookstore.Web --launch-profile vite-manifest
<title>Find a book · Bookstore</title>
<link rel="stylesheet" href="/dist/assets/main-DDpRcdOH.css">
<script type="module" src="/dist/assets/main-DH5dtSTh.js"></script>

Chromium'da sayfa 4 istekte (HTML, CSS, JS ve /api/books) açıldı ve "austen" araması API'deki üç Jane Austen kitabını listeledi. Hash'li dosyalar için Program.cs'teki UseStaticFiles'a bir OnPrepareResponse ekledik; /dist/assets altındaki her yanıt Cache-Control: public, max-age=31536000, immutable ile dönüyor. ASP.NET Core'un kendi derleme zamanı fingerprinting'ini (MapStaticAssets) ASP.NET Core MVC (Model-View-Controller; uygulamayı model, view ve controller katmanlarına ayıran yapı) serisinin "Static Asset'ler ve UI Altyapısı" yazısında anlattık; burada hash'i Vite ürettiği için o mekanizmaya ihtiyaç yok.

import.meta.env ve VITE_ Prefix'i

Vite, build zamanında bilinen değerleri koda import.meta.env üzerinden gömer. Davranışı görmek için widget'ın köküne bir .env dosyası koyup main.ts'e geçici bir satır ekledik:

VITE_STORE_NAME=Bookstore
CATALOG_ADMIN_KEY=local-dev-value
console.log(import.meta.env.VITE_STORE_NAME, import.meta.env.CATALOG_ADMIN_KEY, import.meta.env.MODE);
npx vite build
# console.log(`Bookstore`,void 0,`production`)

npx vite build --mode staging
# console.log(`Bookstore`,void 0,`staging`)

Üç şey görülüyor. VITE_ ile başlayan değişken bundle'a düz metin olarak gömüldü. VITE_ öneki olmayan CATALOG_ADMIN_KEY gömülmedi; import.meta.env.CATALOG_ADMIN_KEY derleme zamanında void 0, yani undefined oldu. MODE, --mode değerini taşıyor; Vite .env.staging gibi mode'a özel dosyaları da bu değere göre yüklüyor. Bu önek kuralı bir güvenlik önlemi: sunucu tarafı için tutulan bir değişken yanlışlıkla client koduna sızmasın diye. Ama tersini de unutma: VITE_ önekli her değer, JavaScript dosyasını indiren herkes tarafından okunabilir. API anahtarı, token ya da şifre hiçbir zaman VITE_ değişkeni olamaz. Bookstore.Web gibi bir backend'in olduğu senaryoda, ortama göre değişen ayarları (API adresi gibi) build'e gömmek yerine sunucunun ürettiği HTML'den okumak daha esnek; widget'ın API adresini data-source attribute'undan almasının sebebi bu. Böylece aynı build çıktısı test ve production ortamlarında değişmeden kullanılabiliyor.

Vite 8'in Production Build'i ve Vite 7'den Geçiş

Vite 8'in (12 Mart 2026) en büyük değişikliği production build'in altında. Vite 7'ye kadar dev tarafında esbuild (pre-bundling ve TypeScript dönüşümü), production'da Rollup çalışıyordu; iki farklı araç, iki farklı davranış demekti. Vite 8'de ikisinin yerini Rust ile yazılan Rolldown aldı. TypeScript dönüşümünü ve JavaScript minification'ını Oxc, CSS minification'ını Lightning CSS yapıyor; Lightning CSS artık opsiyonel değil, normal bir bağımlılık. Rolldown 1.0 ise 7 Mayıs 2026'da kararlı sürüme ulaştı ve public API'si semantic versioning'e bağlandı. Vite ekibinin duyurusundaki rakamlar şirketlerin kendi projelerinden: Linear'ın production build'i 46 saniyeden 6 saniyeye, Ramp'ınki %57, Beehiiv'inki %64 kısalmış. Bu sayılar o projelere ait; serinin son yazısında kendi ölçümlerimizi yapacağız.

build.target'ın varsayılanı 'baseline-widely-available' kaldı ama arkasındaki sürümler yükseldi: Chrome ve Edge 107'den 111'e, Firefox 104'ten 114'e, Safari 16.0'dan 16.4'e. Serinin ilk yazısında bu target'ın CSS nesting'i neden düzleştirdiğini görmüştük.

Vite 7 projesinden geçiş için resmî öneri iki adımlı: önce Vite 7 üzerinde Rolldown'ı deneyen rolldown-vite paketine geç ve sorunları orada çöz, sonra Vite 8'e yükselt. Node.js gereksinimi değişmedi: 20.19+ ya da 22.12+. Vite 8, eski esbuild ve build.rollupOptions ayarlarının bir kısmını Oxc ve Rolldown karşılıklarına kendisi çeviriyor; çoğu projede config değişikliği gerekmiyor. Ama "çoğu" kelimesi önemli. Vite 7'de console çağrılarını production'dan silmenin yaygın yolu esbuild: { drop: ["console"] } idi. Vite 8'de aynı config'i denedik:

esbuild: { drop: ["console"] },
# vite build: hata yok, uyarı yok; çıktıda 2 console.log duruyor

build: { rolldownOptions: { output: { minify: { compress: { dropConsole: true } } } } },
# vite build: çıktıda console.log yok

Eski ayar ne hata ne uyarı verdi; yalnız sessizce etkisiz kaldı. Migration rehberi esbuild.drop'un artık build.rolldownOptions.output.minify.compress altındaki seçeneklere taşındığını yazıyor. Geçişten sonra build çıktısını eski sürümün çıktısıyla karşılaştırmanın sebebi bu. Rehberde ayrıca build.rollupOptions'ın build.rolldownOptions'a yeniden adlandırıldığı, manualChunks'ın object biçiminin kaldırıldığı ve CommonJS default import'larının tek bir kurala bağlandığı yazıyor.

Üretimde Dikkat

Deploy sırasını doğru kur: önce npm ci ve npm run build:server, sonra dotnet publish. wwwroot/dist publish'ten önce dolu olmalı; aksi hâlde yayınlanan uygulama boş bir klasör ve eksik bir manifest ile ayağa kalkar. ViteManifest dosyayı bir kez okuyup bellekte tuttuğu için manifest'i değiştiren her build'den sonra uygulama yeniden başlatılmalı; normal bir deploy zaten bunu yapıyor.

Manifest eksikse hata erken ve açık versin. GetEntry, manifest'te entry bulamazsa npm run build:server çalıştırılıp çalıştırılmadığını soran bir exception fırlatıyor. Sessizce boş bir head üretmek, sayfanın production'da stilsiz ve çalışmayan bir widget'la açılması demek.

Cache başlıklarını ayır. /dist/assets altındaki hash'li dosyalar bir yıl ve immutable; Razor sayfası ise her istekte yeniden üretiliyor ve yeni hash'leri içeriyor. Bir CDN (dosyaları kullanıcıya yakın sunuculardan dağıtan ağ) kullanıyorsan HTML'i uzun süre cache'lememesine dikkat et.

Dev server'ı internete açma. server.host'u açmak, server.cors'u true yapmak ya da server.allowedHosts'u gevşetmek, yerel diskteki kaynak dosyaları ağdaki başkalarına açabilir. Vite dev server bir geliştirme aracı; production'da her zaman build çıktısı sunulmalı.

Sık Düşülen Tuzaklar

Birincisi, VITE_ öneki gizlilik sanmak. Önek bir değişkeni client'a açmanın yoludur, gizlemenin değil. VITE_ ile başlayan her değer bundle'da düz metin olarak durur.

İkincisi, proxy anahtarını fazla geniş yazmak. "/api", /api.html'i ve /apiary'yi de yakalar. Anahtarı "/api/" olarak yaz ya da ^ ile başlayan bir regular expression kullan.

Üçüncüsü, HMR'ın her değişiklikte state'i koruyacağını beklemek. Framework'süz kodda yalnız CSS yerinde güncellenir; JavaScript değişikliği HMR sınırı olmadığı için sayfayı yeniden yükler. Bu bir hata değil, Vite'ın güvenli tercihi.

Dördüncüsü, dev ile production arasındaki farkı test etmemek. Dev server native ESM ve pre-bundle edilmiş bağımlılıklarla, production build ise Rolldown'ın tree shaking'i ve chunk'larıyla çalışır. Bir paketin CommonJS uyumluluğu ya da side effect'i bir tarafta sorun çıkarıp diğerinde çıkarmayabilir. CI'da production build'i en az bir smoke test ile tarayıcıda açmak bu farkı yakalar.

Beşincisi, Vite 8'e geçişte eski esbuild ayarlarına güvenmek. Yukarıda gördüğümüz gibi bazıları hata vermeden etkisiz kalıyor; geçişten sonra çıktıyı kontrol et.

Küçük Bir Deneme

★☆☆ npm run dev ile Vite'ı, dotnet run --project server/Bookstore.Web --launch-profile vite-dev ile Bookstore.Web'i başlat. http://localhost:5080'de arama kutusuna bir şey yaz, sonra styles.css'te bir rengi ve format.ts'te para birimi biçimini değiştir; hangisinde yazdığın metnin kaldığını not et.

★★☆ main.ts'e import.meta.hot.accept() çağrısı ekleyerek widget'ı yeniden bağlayan bir HMR sınırı kur: yeni mountBookFinder geldiğinde host element'i temizleyip yeniden çağır. finder.ts değişikliğinin artık sayfayı yeniden yüklemediğini doğrula; ardından bu kodun webpack ve esbuild build'lerini bozup bozmadığını kontrol et.

★★★ Widget'a dinamik import ile yüklenen ikinci bir module ekle (örneğin bir kitabın ayrıntılarını gösteren panel). npm run build:server sonrasında manifest'te dynamicImports ve yeni chunk'ın nasıl göründüğüne bak; tag helper'ın ürettiği etiketlerin hâlâ doğru olduğunu Chromium'da doğrula.

Bir sonraki yazıda bu dört yazıda gördüğümüz parçaların Angular dünyasındaki karşılığına geçiyoruz: Angular CLI'ın application builder'ı, esbuild ve Vite'ı nasıl birleştirdiği, Rolldown'a geçiş ve webpack tabanlı builder'dan migration.