Vite, serwer deweloperski i bundler w jednym
Vite to narzędzie budujące dla aplikacji przeglądarkowych, które łączy dwie osobne rzeczy: szybki serwer deweloperski oparty o natywne moduły ES oraz bundler produkcyjny. Rozwija je zespół skupiony wokół firmy VoidZero, licencja to MIT, bieżąca wersja to 8.2.1, a repozytorium ma około 82 tysięcy gwiazdek.
Co Vite właściwie robi
Nazwa sugeruje jedno narzędzie, a w środku pracują dwa różne mechanizmy, i to jest pierwsze źródło nieporozumień.
Pierwszy mechanizm to serwer deweloperski. Kiedy uruchamiasz projekt lokalnie, Vite nie buduje paczki z całej aplikacji. Zamiast tego podaje przeglądarce pliki źródłowe jako moduły ES i przekształca je pojedynczo, dokładnie w momencie, w którym przeglądarka o nie poprosi. Plik, którego nikt nie zaimportował, nigdy nie zostaje przetworzony. Dlatego czas startu serwera prawie nie zależy od rozmiaru projektu, co przy dużej aplikacji bywa różnicą liczoną w minutach.
Drugi mechanizm to budowanie produkcyjne, gdzie podejście jest odwrotne. Powstaje klasyczna paczka, z podziałem na fragmenty, usuwaniem nieużywanego kodu i minifikacją. Powód tej asymetrii jest czysto praktyczny: setki nieopakowanych modułów ES działają znakomicie na lokalnym dysku i fatalnie przez sieć, gdzie każdy z nich oznacza osobne żądanie i osobne opóźnienie.
Trzecia rzecz, o której łatwo zapomnieć, to wstępne pakowanie zależności. Biblioteki z katalogu node_modules zmieniają się rzadko, a bywają rozbite na setki plików. Vite pakuje je raz, przy pierwszym uruchomieniu, i wynik trzyma w katalogu node_modules/.vite. Dzięki temu przeglądarka pobiera jeden plik zamiast trzystu, a przy okazji moduły zapisane w starym formacie CommonJS zostają zamienione na moduły ES, których przeglądarka i tak wymaga.
Czego Vite nie robi, jest równie ważne. Nie jest frameworkiem i nie narzuca struktury aplikacji, więc nadaje się pod React, Astro czy SvelteKit tak samo dobrze jak pod projekt bez żadnej biblioteki. Nie uruchamia testów, choć Vitest korzysta z tej samej konfiguracji i tego samego łańcucha wtyczek. Nie zarządza monorepozytorium, od tego są narzędzia w rodzaju Turborepo. Nie dostarcza też gotowego serwera produkcyjnego: tryb renderowania po stronie serwera to zestaw interfejsów programistycznych, a nie zamknięte rozwiązanie.
Dwa tryby, dwa różne mechanizmy
Rozdzielenie trybu deweloperskiego od produkcyjnego jest źródłem zarówno największej zalety narzędzia, jak i najbardziej irytującej klasy błędów.
Zaleta jest oczywista przy pracy. Aktualizacja modułu po zapisaniu pliku dotyczy tylko tego modułu i jego bezpośrednich odbiorców. Nie ma przebudowy całego grafu, więc czas reakcji nie rośnie razem z projektem, a stan aplikacji przy podmianie modułu zwykle zostaje zachowany.
Wada wychodzi później. Skoro to dwie różne ścieżki przetwarzania, mogą się różnić wynikiem. Kod, który działa lokalnie i psuje się po zbudowaniu, to najczęściej efekt jednej z trzech rzeczy. Pierwsza to zależność, która deklaruje inny plik wejściowy dla przeglądarki niż dla Node, a warunki eksportu rozstrzygają się w obu trybach inaczej. Druga to kolejność wczytywania arkuszy stylów, która przy nieopakowanych modułach idzie za kolejnością żądań, a po zbudowaniu za kolejnością w grafie zależności. Trzecia to kod, który zakłada, że pliki pozostaną osobne, na przykład ścieżki składane ręcznie z fragmentów tekstu zamiast przez new URL z import.meta.url.
Wniosek praktyczny jest krótki: vite build i vite preview uruchamiane lokalnie, zanim zmiana trafi do repozytorium, wyłapują większość takich przypadków w kilkadziesiąt sekund. Zespoły, które robią to dopiero na serwerze budującym, płacą za tę oszczędność w postaci cyklu poprawek liczonego w godzinach.
Rolldown, czyli co zmieniła wersja 8
Do wersji 7 włącznie Vite opierało się na dwóch osobnych silnikach: esbuild przetwarzał zależności i transformacje w trybie deweloperskim, a Rollup budował paczkę produkcyjną. Ten podział był świadomym kompromisem z czasów pierwszych wydań, ale kosztował rozjazd między tym, co widzisz lokalnie, a tym, co dostajesz po zbudowaniu.
Wersja 8, wydana 12 marca 2026 roku, kończy ten podział. Bundlerem jest teraz Rolldown, napisany w Rust, a transformacje i minifikację przejęły narzędzia z rodziny Oxc. Widać to wprost w zależnościach pakietu: vite w wersji 8.2.1 wymaga rolldown w wersji około 1.2, natomiast rollup zniknął z zależności całkowicie, a esbuild zszedł do roli opcjonalnej zależności równorzędnej, deklarowanej jako peerDependencies z oznaczeniem optional. Minifikacja arkuszy stylów domyślnie idzie przez Lightning CSS.
Zmiana ma bezpośrednie konsekwencje w konfiguracji. Pole build.rollupOptions zostało zastąpione przez build.rolldownOptions, optimizeDeps.esbuildOptions przez optimizeDeps.rolldownOptions, a sekcja esbuild przez sekcję oxc. Stare nazwy nadal są rozpoznawane i tłumaczone wewnętrznie, ale oznaczono je jako przestarzałe. Zmieniły się też domyślne wartości: build.target to teraz 'baseline-widely-available', a build.minify przyjmuje domyślnie 'oxc' dla budowania po stronie klienta i false dla budowania na potrzeby renderowania serwerowego.
Liczby podawane przy wydaniu pochodzą od zespołów, które testowały wersję zapowiadaną. Linear raportuje skrócenie czasu budowania produkcyjnego z 46 sekund do 6 sekund, Ramp około 57 procent, Beehiiv około 64 procent, a Mercedes-Benz.io do 38 procent. Skala zależy od projektu i najmocniej widać ją tam, gdzie budowanie było wcześniej ograniczone czasem pracy procesora, a nie liczbą operacji dyskowych.
Jest też koszt, o którym warto wiedzieć zawczasu. Vite 8 zajmuje po instalacji około 15 megabajtów więcej niż Vite 7, z czego mniej więcej 10 megabajtów to Lightning CSS, a 5 megabajtów sam Rolldown. Przy obrazach kontenerów budowanych od zera na każde zgłoszenie zmian to widoczna pozycja.
Warto rozróżnić dwie rzeczy, które w opisach z 2025 roku często się mieszają. Pakiet rolldown-vite był osobną wersją zapowiadaną, publikowaną w czasie, gdy Rolldown nie był jeszcze domyślny, i jego rozwój zatrzymał się na wersji 7.3.1. Dziś nie ma powodu, żeby po niego sięgać: w wersji 8 Rolldown jest bundlerem domyślnym i jedynym, bez żadnej flagi włączającej.
Instalacja i wymagania
Vite 8 wymaga Node w wersji 20.19 lub nowszej z linii 20, albo 22.12 lub nowszej. Starsze wydania Node nie uruchomią się w ogóle, co przy serwerach budujących z przypiętą wersją bywa pierwszym problemem podczas aktualizacji.
# nowy projekt z szablonem wybieranym interaktywnie
npm create vite@latest moja-aplikacja
# szablon podany wprost, bez pytań
npm create vite@latest moja-aplikacja -- --template react-ts
# dodanie do istniejącego projektu
npm install --save-dev vite
# uruchomienie serwera deweloperskiego, budowanie i podgląd wyniku
npx vite
npx vite build
npx vite previewSerwer deweloperski nasłuchuje domyślnie na porcie 5173. Jeśli port jest zajęty, Vite przechodzi na kolejny wolny, chyba że ustawisz server.strictPort, które w takiej sytuacji przerywa uruchomienie zamiast po cichu zmieniać adres.
Konfiguracja i nazwy, które trzeba znać
Konfiguracja mieszka w pliku vite.config.ts w katalogu głównym projektu. Funkcja defineConfig nie robi nic w czasie działania, służy wyłącznie do podpowiedzi typów, więc korzystanie z niej w projekcie na TypeScript opłaca się od pierwszego dnia.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': '/src' },
dedupe: ['react', 'react-dom']
},
server: {
port: 5173,
strictPort: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
},
warmup: {
clientFiles: ['./src/main.tsx', './src/components/**/*.tsx']
}
},
optimizeDeps: {
include: ['lodash-es'],
exclude: ['@moja-firma/pakiet-lokalny']
},
build: {
target: 'baseline-widely-available',
sourcemap: true,
rolldownOptions: {
output: {
codeSplitting: true
}
}
}
})Trzy pola z tego przykładu wracają najczęściej w zgłoszeniach problemów. resolve.dedupe rozwiązuje sytuację, w której dwie kopie tej samej biblioteki trafiają do paczki, co przy bibliotekach trzymających stan globalny kończy się awarią bez sensownego komunikatu. optimizeDeps.exclude jest potrzebne dla pakietów z tego samego monorepozytorium, bo wstępne pakowanie zamraża ich stan i zmiany w kodzie źródłowym przestają być widoczne. server.warmup przetwarza wskazane pliki z wyprzedzeniem, zamiast czekać na pierwsze żądanie z przeglądarki.
Podział na fragmenty konfiguruje się przez build.rolldownOptions.output.codeSplitting, a szczegółowe opcje opisuje dokumentacja Rolldown, nie Vite. To ważne przy przenoszeniu konfiguracji z wersji 7, bo dawne ustawienia manualChunks z Rollupa nie przenoszą się jeden do jednego.
Zmienne środowiskowe i dane wstrzykiwane do kodu
Do kodu przeglądarkowego trafiają wyłącznie zmienne z przedrostkiem VITE_. To zabezpieczenie, nie utrudnienie: bez niego wystarczyłby jeden nieostrożny odczyt, żeby klucz z pliku .env wylądował w paczce wysyłanej użytkownikom.
// pliki wczytywane w kolejności: .env, .env.local, .env.[mode], .env.[mode].local
const apiUrl = import.meta.env.VITE_API_URL
// wbudowane pola dostępne zawsze
console.log(import.meta.env.MODE) // 'development' albo 'production'
console.log(import.meta.env.BASE_URL) // wartość opcji base
console.log(import.meta.env.PROD) // wartość logiczna
console.log(import.meta.env.DEV)
console.log(import.meta.env.SSR)
// import wielu plików naraz, rozwijany w czasie budowania
const strony = import.meta.glob('./strony/*.tsx')
const ikony = import.meta.glob('./ikony/*.svg', { eager: true })
// poprawne budowanie ścieżki do zasobu
const dzwiek = new URL('./assets/klik.mp3', import.meta.url).hrefWywołanie import.meta.glob jest rozwijane w czasie budowania, więc wzorzec musi być zapisany dosłownie. Przekazanie tam zmiennej nie zadziała i jest to jedna z częstszych pomyłek przy przenoszeniu kodu z webpacka, gdzie require.context zachowywał się nieco swobodniej.
Wtyczki i punkty zaczepienia
System wtyczek Vite rozszerza interfejs wtyczek Rollupa o kilka własnych zaczepień. Dzięki temu duża część istniejących wtyczek działa bez zmian, a te specyficzne dla Vite dokładają obsługę serwera deweloperskiego.
import type { Plugin } from 'vite'
export function wtyczkaZnacznikaCzasu(): Plugin {
const wirtualnyModul = 'virtual:czas-budowania'
return {
name: 'znacznik-czasu',
enforce: 'pre', // 'pre' albo 'post'
apply: 'build', // 'build' albo 'serve'
config(uzytkownika, { command }) {
return { define: { __KOMENDA__: JSON.stringify(command) } }
},
configResolved(gotowa) {
console.log('tryb:', gotowa.mode)
},
resolveId(id) {
return id === wirtualnyModul ? '\0' + wirtualnyModul : null
},
load(id) {
if (id !== '\0' + wirtualnyModul) return null
return `export const czas = ${Date.now()}`
},
transformIndexHtml(html) {
return html.replace('</head>', '<meta name="build" content="ok"></head>')
},
configureServer(server) {
server.middlewares.use('/health', (_req, res) => res.end('ok'))
}
}
}Pole enforce decyduje o kolejności względem wtyczek wbudowanych, a apply pozwala ograniczyć wtyczkę do jednego trybu. Ta druga opcja bywa niedoceniana: wtyczka analizująca rozmiar paczki nie ma czego robić przy serwerze deweloperskim, a jej obecność tam potrafi wyraźnie spowolnić pierwsze wczytanie strony. Nazwa name nie jest ozdobnikiem, pojawia się w komunikatach błędów i w wynikach profilowania.
Vite a alternatywy
| Cecha | Vite | webpack | Rspack | Parcel | Turbopack |
|---|---|---|---|---|---|
| Silnik | Rolldown i Oxc, Rust | JavaScript | Rust | Rust i SWC | Rust |
| Tryb deweloperski | natywne moduły ES | pełne pakowanie | pełne pakowanie | pełne pakowanie | pakowanie przyrostowe |
| Konfiguracja | mała, jeden plik | rozbudowana | zgodna z webpackiem | bez konfiguracji | powiązana z Next.js |
| Ekosystem wtyczek | Rollup i Vite | największy | wtyczki webpacka | własny | wewnętrzny |
| Typowe zastosowanie | aplikacje i biblioteki | starsze projekty | migracja z webpacka | małe projekty | Next.js |
| Licencja | MIT | MIT | MIT | MIT | MPL 2.0 |
Wybór rozstrzyga się zwykle na dwóch pytaniach. Jeśli masz działającą konfigurację webpacka i setki linii reguł ładowania, Rspack pozwala zachować je niemal w całości, a Vite wymaga przepisania od nowa. Jeśli zaczynasz projekt albo używasz frameworka, który i tak stoi na Vite, na przykład Nuxt, pytanie o alternatywy przestaje mieć praktyczne znaczenie.
Typowe błędy
Pierwszy to mylenie serwera deweloperskiego z serwerem produkcyjnym. Polecenie vite preview służy do sprawdzenia zbudowanej paczki na własnej maszynie i nie ma zabezpieczeń wymaganych od usługi wystawionej na świat. Statyczne pliki z katalogu dist powinien podawać właściwy serwer HTTP albo sieć dostarczania treści.
Drugi to zmienne środowiskowe odczytywane przez process.env w kodzie przeglądarkowym. W przeglądarce ten obiekt nie istnieje, a Vite nie podstawia go automatycznie. Jedyną poprawną drogą jest import.meta.env z przedrostkiem VITE_, ewentualnie pole define dla wartości ustalanych w czasie budowania.
Trzeci to nieaktualne wstępne pakowanie zależności. Po zmianie wersji biblioteki albo po ręcznej edycji czegoś w node_modules zawartość katalogu node_modules/.vite przestaje odpowiadać rzeczywistości, a objawem są importy wskazujące na kod, którego już nie ma. Uruchomienie z flagą --force odtwarza ten katalog od zera i rozwiązuje sprawę w kilkanaście sekund.
Czwarty to zapominanie o opcji base przy wdrożeniu pod podkatalogiem. Domyślna wartość zakłada, że aplikacja stoi w katalogu głównym domeny. Jeśli trafia pod adres z dodatkowym segmentem ścieżki, wszystkie odwołania do zasobów wskazują w próżnię, a strona ładuje się jako sam dokument HTML bez stylów i skryptów.
Piąty dotyczy arkuszy stylów przetwarzanych przez narzędzia zewnętrzne. Konfiguracja Tailwind CSS czy innego preprocesora jest osobną warstwą, a Vite tylko ją uruchamia. Wolne przebudowy stylów prawie nigdy nie wynikają z samego Vite, tylko ze zbyt szeroko określonych wzorców plików w konfiguracji tej warstwy.
Szósty to instalowanie wtyczek dla samego uruchomienia menedżera pakietów innego niż domyślny. Vite działa na Node, ale uruchamia się też przez Bun czy pnpm bez dodatkowych wtyczek, a różnice sprowadzają się do czasu instalacji zależności, nie do sposobu budowania.
FAQ - najczęściej zadawane pytania
Czy w wersji 8 trzeba włączać Rolldown osobno?
Nie. W Vite 8 Rolldown jest bundlerem domyślnym i jedynym, bez żadnej flagi. Pakiet rolldown-vite, publikowany w czasie fazy zapowiadanej, nie jest już potrzebny i zatrzymał się na wersji 7.3.1.
Czy migracja z wersji 7 wymaga przepisania konfiguracji?
W typowym projekcie nie. Nazwy build.rollupOptions i optimizeDeps.esbuildOptions są nadal rozpoznawane i tłumaczone wewnętrznie na nowe odpowiedniki. Przepisania wymagają szczegółowe ustawienia podziału na fragmenty oraz wtyczki sięgające bezpośrednio do interfejsów Rollupa.
Dlaczego aplikacja działa lokalnie, a po zbudowaniu jest pusta?
Najczęściej z powodu opcji base niedopasowanej do adresu wdrożenia albo błędu w kodzie, który ujawnia się dopiero po minifikacji. Uruchom vite build i vite preview lokalnie, a potem sprawdź zakładkę sieci w narzędziach przeglądarki: żądania kończące się kodem 404 wskazują na base, a błąd w konsoli na drugą przyczynę.
Czy Vite nadaje się do budowania bibliotek?
Tak, służy do tego tryb biblioteki konfigurowany przez build.lib z polami entry, name, fileName i formats. Domyślne formaty zależą od liczby punktów wejścia: przy jednym powstają es i umd, przy wielu es i cjs.
Jak przyspieszyć start serwera w dużym projekcie?
Zacznij od server.warmup dla plików wczytywanych zawsze, a następnie sprawdź, czy któraś zależność nie jest niepotrzebnie wyłączona z pakowania wstępnego. Odwrotny ruch, czyli dopisanie ciężkich pakietów do optimizeDeps.include, pomaga wtedy, gdy Vite odkrywa je dopiero w trakcie pracy i przeładowuje stronę.
Czy Vite obsługuje renderowanie po stronie serwera?
Obsługuje, ale w formie interfejsów programistycznych, na których buduje się rozwiązanie docelowe. Gotową obsługę dostarczają frameworki stojące na Vite, więc przy projekcie produkcyjnym rozsądniej jest sięgnąć po jeden z nich niż składać własny serwer od zera.
Dokumentację znajdziesz na stronie Vite, zapowiedź wersji 8 w blogu projektu, a kod źródłowy w repozytorium na GitHubie.