Kontekst projektowy: gdzie CI dla frontendu się sypie i dlaczego
Typowy zespół frontendowy ma te same zgryzoty: buildy działają lokalnie, ale pękają w CI; czas kompilacji rośnie z każdym sprintem; ważące megabajty bundle wylatują na produkcję bez ostrzeżenia; cache niby jest, ale losowo nie działa. Do tego dochodzą przydługie instalacje zależności i brak powtarzalności między gałęziami. Sercem problemu jest brak inżynierii wokół procesu budowania: brak deterministycznego środowiska, kiepsko zaprojektowany cache i brak automatycznej analizy bundli.
Cel: ułożyć DevOps dla frontendu tak, by Vite, webpack i esbuild dawały szybkie, przewidywalne i mierzalne buildy. Poniżej praktyczny plan bez wodolejstwa, z gotowymi kawałkami konfiguracji i listą najczęstszych pułapek.
Plan działania i pytania, które trzeba rozstrzygnąć
- Jak zagwarantować deterministyczny build (ta sama wersja node, lockfile, flags)?
- Jak ułożyć cache: zależności, kompilacja TypeScript, cache webpacka, cache Vite (Rollup), artefakty?
- Który bundler gdzie ma sens: Vite vs webpack vs esbuild w CI?
- Jak wdrożyć analizę bundli i progi wagowe w PR-ach?
- Jak skrócić buildy w monorepo i uniknąć przebudowy wszystkiego?
- Jak kontrolować i raportować rozmiar bundla na branchach i w releasach?
- Jak nie wpaść w typowe pułapki (np. zły cache-key, brak pruning w pnpm, nieczytelne timingi)?
Porównanie bundlerów w realnym CI: szybkość, deterministyczność, funkcje
Vite (Rollup + esbuild) – szybka ścieżka prod i dobre pluginy
Vite w trybie produkcyjnym opiera się o Rollup i często używa esbuild do transpilacji/ts i minifikacji (w nowszych wersjach Terser/SWC bywa alternatywą). Daje czyste outputy, świetnie współpracuje z code-splittingiem i dynamic import. W CI Vite jest rozsądnym wyborem dla SPA/MPA, gdy zależy nam na:
- dobrym tree-shakingu i stabilnych sourcemapach,
- łatwej integracji z rollup-plugin-visualizer do analizy bundla,
- przewidywalnym cache node_modules i cache wyników pre-bundlingu (optimizeDeps).
Pułapki: niestabilny cache optimizeDeps między gałęziami, błędne rozpoznawanie CommonJS u części paczek, zbyt agresywny split vendorów prowadzący do przeciążenia sieci (zbyt wiele chunków).
webpack – elastyczność i długa historia w ekosystemie
webpack to nadal najpopularniejszy bundler w dużych projektach i mikrofrontendach. Ma rozbudowany cache na dysku, generuje plik stats.json do dogłębnej analizy oraz potężny ekosystem pluginów. W CI sprawdza się w projektach, gdzie:
- mamy customowe ładowarki (np. legacy CSS Modules, SVG),
- potrzebna jest kompatybilność ze starym kodem CommonJS i feature’ami specyficznymi dla webpacka,
- pragniemy wykorzystać persistent cache i deterministyczny hashing assetów.
Pułapki: błędne klucze cache (cacheName, buildDependencies), brak izolacji cache między gałęziami, pluginy generujące niedeterministyczne outputy (np. oparte o czas). Dodatkowo, domyślne devtool potrafi drastycznie spowolnić build w CI, jeśli generujemy pełne sourcemapy.

esbuild – ultraszybkie kompilacje i analiza przez metafile
esbuild kompiluje w Go i wygrywa na czasie. Ma wbudowaną analizę przez –metafile i potrafi samodzielnie zbudować prostą aplikację SPAs/MPAs. Świetny jako:
- narzędzie do pre-bundlingu dependency (np. narzędzia, biblioteki wewnętrzne),
- krok optymalizacji TypeScript/JS lub CSS (minify),
- silnik budujący biblioteki (mniejsze overheady niż Rollup przy prostych przypadkach).
Pułapki: brak zaawansowanych pluginów jak w Rollup/webpack (choć ekosystem rośnie), niuanse z CommonJS/ESM w niektórych paczkach, ograniczony model rozwiązywania bardziej skomplikowanych scenariuszy bundlowania.
Krótka tabela porównawcza pod kątem CI
| Narzędzie | Szybkość build prod | Cache wbudowany | Analiza bundla | Tryb użycia w CI |
|---|---|---|---|---|
| Vite (Rollup) | Wysoka | Tak (pośrednio: optimizeDeps, cache Rollup) | Pluginy (visualizer) | SPA/MPA, nowoczesne projekty |
| webpack | Średnia–wysoka (z optymalizacjami) | Tak (persistent cache) | stats.json, bundle-analyzer | Duże projekty, custom pipeline |
| esbuild | Bardzo wysoka | Brak persistent cache jak w webpack | metafile | Narzędzia, liby, szybkie bundlowanie |
Deterministyczne środowisko: wersje, lockfile, zmienne i systemy
Wersje Node i menedżerów pakietów przypięte jak do betonu
Brak spójnej wersji Node i pnpm/yarn/npm to główna przyczyna „działa u mnie, nie działa w CI”. Minimalny zestaw:
- .nvmrc lub .node-version (Volta/asdf) z konkretną wersją LTS,
- pin wersji pnpm/yarn/npm w CI (instalacja określonej wersji),
- Docker z pinned image (np. node:20.11-alpine z konkretną subwersją).
Bez tego nie ma co marzyć o powtarzalności. Jeśli Docker jest w użyciu, obraz budujący musi być wersjonowany i aktualizowany kontrolowanie (np. przez Renovate).
Lockfile jako jedyne źródło prawdy
Używaj –frozen-lockfile (yarn), –frozen-lockfile/–lockfile-only (pnpm) lub ci (npm) tak, by CI nie generował nowego locka. Scenariusz:
- commit zaktualizowanego lockfile w PR,
- na CI instalacja tylko na podstawie locka,
- jeśli lock nieaktualny – fail szybko, nie po 15 minutach builda.
Pułapka: korzystanie z mirrorów registry lub prywatnych rejestrów bez wersjonowania i bez 100% zgodności z publicznym npm. Jeśli mirror jest niestabilny, buildy są niestabilne. Zadbaj o fallback oraz stałe endpointy z cache na poziomie proxy.
Browserslist, NODE_ENV i inne dźwignie deterministyczności
Te same pliki konfiguracyjne (browserslist, babel, tsconfig, postcss) muszą być wersjonowane i bezwarunkowo respektowane w CI. Ustal:
- NODE_ENV=production podczas buildów produkcyjnych,
- BUILD_ID lub SOURCE_DATE_EPOCH, jeśli potrzebna jest bitowa powtarzalność (reproducible builds),
- LANG/LC_ALL (unikaj niespodzianek z formatami).
Jeśli używasz minifikatorów, które mogą wstrzykiwać timestampy, przełącz je w tryby deterministyczne lub usuń timestampy (np. z bannerów).
Cache w CI: co naprawdę przyspiesza, a co udaje, że przyspiesza
Cache zależności: node_modules i cache menedżera
Największy zysk to poprawne keszowanie instalacji. Dwa poziomy:
- cache narzędzia (pnpm-store, yarn cache, npm cache),
- cache node_modules lub w pnpm – hermetycznego store + pnpm-lock.yaml jako klucza.
Klucz do cache musi zawierać skrót lockfile (pnpm-lock.yaml/yarn.lock/package-lock.json) oraz wersję Node i menedżera pakietów. W przeciwnym razie mamy „zamulony cache” lub – gorzej – trudne do śledzenia błędy w runtime.
Cache kompilacji: webpack persistent cache i Rollup cache
Dla webpacka włącz persistent cache na dysku z poprawnym buildDependencies. Przykład:
cache: {
type: 'filesystem',
cacheDirectory: '.cache/webpack',
buildDependencies: {
config: [__filename],
tsconfig: ['tsconfig.json'],
lock: ['pnpm-lock.yaml']
}
}
Kluczową pułapką jest brak invalidacji przy zmianie loadera lub wtyczek. Dodaj je do buildDependencies. Dla Rollupa pamiętaj, że cache przydaje się głównie w trybie watch; w czystym CI warto oprzeć się na keszu zależności i unikaniu niepotrzebnej pracy (np. dzielone bundlowanie vendorów w monorepo).
Artefakty jako cache pośredni
Jeśli pipeline jest wieloetapowy (testy → build → analiza → deploy), publikuj efekt builda jako artefakt i używaj w kolejnych krokach. To bywa bardziej stabilne niż próby współdzielenia wewnętrznych katalogów cache w różnych runnerach. Pamiętaj o rozdzieleniu artefaktów per-branch i per-platform, żeby nie mieszać buildów.
Konfiguracje CI: GitHub Actions, GitLab CI i CircleCI – gotowe bloki
GitHub Actions: szybki start z cache i analizą bundla
Przykładowy workflow dla pnpm + Vite + analiza bundla:






