Twoje wizualne testy regresji przechodzą na laptopie. Robisz push, pipeline rusza i te same testy albo po cichu wywalają przeglądarkę, albo zgłaszają pięćdziesiąt „zmian”, które w rzeczywistości są tylko różnicami w antyaliasingu. W kodzie nic się nie zmieniło, ale CI ma inne zdanie.
Ta przepaść między „działa lokalnie” a „działa w CI” to niemal zawsze problem infrastruktury, a nie testów. Żeby niezawodnie hostować wizualne testy regresji we własnym pipelinie CI, potrzebujesz czterech poprawnie połączonych elementów: runnera CI z executorem Docker, kontenera skonfigurowanego tak, żeby headless Chromium nie zabrakło pamięci współdzielonej, narzędzia do porównywania obrazów generującego raporty czytelne dla CI oraz VPS-a dobranego pod pamięć, którą przeglądarki headless faktycznie zużywają.
Ten przewodnik buduje ten stos od początku do końca. Na koniec będziesz mieć działający .gitlab-ci.yml (albo jego odpowiednik dla Gitea), środowisko Docker, w którym Chromium przestaje się wywalać, BackstopJS generujący raporty JUnit czytelne dla pipeline'u oraz odpowiednio dobrany VPS, żeby to wszystko uruchomić.
TL;DR
- Uruchamiaj przeglądarkę w tym samym obrazie Docker lokalnie i w CI. Większość fałszywych różnic bierze się z rozbieżności w czcionkach i renderowaniu między środowiskami. Przypnij obraz, a znikną.
- Napraw domyślne 64MB dla /dev/shm. Domyślny rozmiar pamięci współdzielonej w narzędziu Docker głodzi renderer Chromium, który po cichu pada w CI. Ustaw shm_size: '2gb' w docker-compose.yml albo przekaż Chromium flagę --disable-dev-shm-usage.
- Postaw na BackstopJS jako główne narzędzie do samodzielnego hostowania. Ma licencję MIT (v6.3.25), stawia na Dockera dzięki fladze --docker i generuje raporty JUnit. Jeśli już używasz Playwright, jego wbudowane toHaveScreenshot() to dobry punkt wyjścia bez żadnej instalacji.
- Przyjmij 4GB RAM jako punkt wyjścia. Planuj pamięć z zapasem. W praktyce zadanie robiące zrzuty ekranu w Chromium potrafi zużyć podczas przechwytywania całej strony znacznie więcej pamięci, niż wygląda na potrzebne w spoczynku, więc 4GB RAM to bezpieczniejsza baza dla jednego lub dwóch zadań sekwencyjnych albo lekko zrównoleglonych.
Czego ten przewodnik nie obejmuje
To przewodnik wdrożeniowy dla infrastruktury, którą już postanowiłeś postawić. Kilka rzeczy celowo zostawiamy poza zakresem:
- To nie jest pełne porównanie narzędzi. Ten przewodnik skupia się na stosie, który realnie da się obsłużyć w samodzielnie hostowanym środowisku CI: BackstopJS, Docker i własny runner.
- Nie obejmuje samodzielnego hostowania Argos. Argos jest open source, ale jego publiczna ścieżka produktowa i dokumentacja skupiają się na hostowanej aplikacji Argos i integracjach z CI, a nie na prostej produkcyjnej ścieżce samodzielnego hostowania. Do celów tego przewodnika BackstopJS jest bezpieczniejszym przykładem.
- Nie rekomendujemy Lost Pixel. Ten projekt został zarchiwizowany w kwietniu 2026 roku. Do nowych wdrożeń używaj BackstopJS.
- Nie wraca do sporu o samodzielne hostowanie kontra zarządzany SaaS. Skoro tu jesteś, to Percy i Chromatic masz już za sobą.
Wymagania wstępne
Zanim wpiszesz pierwsze polecenie, przygotuj kilka rzeczy:
- Samodzielnie hostowany runner CI albo plan jego wdrożenia (opisany niżej).
- Docker zainstalowany na hoście runnera.
- Projekt Node.js z istniejącym celem testów (adres URL działającej aplikacji albo zestaw ścieżek komponentów do przechwycenia).
- Dostęp do powłoki na VPS-ie lub hoście, na którym działa runner.
Dlaczego testy wizualne przechodzą lokalnie, a psują się w CI
Przeglądarka renderująca wzorzec na macOS to nie ta sama przeglądarka, która renderuje porównanie w linuksowym kontenerze CI. Dokumentacja Playwright mówi o tym wprost: renderowanie może się różnić w zależności od OS hosta, wersji, ustawień, sprzętu, źródła zasilania, trybu headless i innych czynników. Rekomendacja jest równie bezpośrednia: żeby zrzuty ekranu były spójne, uruchamiaj testy w tym samym środowisku, w którym wygenerowano wzorcowe zrzuty ekranu.
Ta jedna rekomendacja jest powodem, dla którego cała reszta tej konfiguracji w ogóle istnieje. Za niemal każdą fałszywą porażkę odpowiadają trzy rozbieżności:
- Różnice w czcionkach i antyaliasingu. Twoja maszyna deweloperska i kontener CI mają inne pakiety czcionek i inne renderowanie subpikselowe. Strony z dużą ilością tekstu różnią się przy każdym uruchomieniu, nawet gdy nic się nie zmieniło.
- Renderowanie headless kontra z interfejsem. Przeglądarka z interfejsem i ta w trybie headless mogą nieco inaczej rozłożyć tę samą stronę.
- Wyczerpanie pamięci współdzielonej. To ten cichy zabójca. Domyślny /dev/shm w Dockerze ma 64MB, a headless Chromium używa pamięci współdzielonej dla procesów renderujących. Gdy jej zabraknie, Chromium pada bez żadnego użytecznego komunikatu. Zadanie po prostu umiera albo zawisa.
Pierwsze dwa problemy rozwiązuje przypięcie jednego obrazu Docker zarówno do generowania wzorców, jak i do porównań. Trzeci rozwiązujemy w sekcji o konfiguracji Dockera poniżej. Napraw wszystkie trzy, a testy, które były „niestabilne w CI”, staną się deterministyczne.

Wybór narzędzia: BackstopJS czy wbudowane porównywanie w Playwright
Jeśli używasz już Playwright do testów end-to-end, najszybszą drogą jest jego wbudowana asercja toHaveScreenshot(): zero dodatkowych zależności, porównywanie na poziomie pikseli i wzorce z sufiksem platformy od ręki. Dla małego zestawu testów to całkiem sensowne miejsce, żeby się zatrzymać, a zespołom z mniej więcej pięćdziesięcioma ekranami i bez współdzielonej biblioteki komponentów może to wystarczyć na zawsze.
Droga kończy się przy procesie przeglądu. Porównywanie w Playwright działa wyłącznie na pikselach i nie ma UI do przeglądu w PR, więc zatwierdzenie porcji uprawnionych zmian wizualnych oznacza ręczne regenerowanie snapshotów. Gdy liczba wzorców rośnie, ta ręczna pętla akceptacji zaczyna boleć.
BackstopJS to dedykowane narzędzie zbudowane dokładnie pod tę pętlę. Ma licencję MIT (obecnie v6.3.25), stawia na Dockera dzięki fladze --docker i generuje JUnit XML, który runner CI czyta natywnie. Jego przepływ pracy z wzorcami (wygeneruj referencję, przetestuj względem niej, zatwierdź zaakceptowane różnice jako nowy wzorzec) to model operacyjny, do którego ostatecznie dochodzi większość samodzielnie hostowanych konfiguracji VRT. W dalszej części przewodnika BackstopJS jest przykładem roboczym.
Dwa narzędzia, które możesz spotkać w rekomendacjach gdzie indziej, odpadają z tego poradnika: Argos nie jest tu omawiany, bo jego publiczna ścieżka produktowa koncentruje się na hostowanej aplikacji i integracjach z CI, a Lost Pixel jest zarchiwizowany. BackstopJS to bezpieczniejszy przykład roboczy dla praktycznej konfiguracji na własnym serwerze.
| Oś | BackstopJS | Playwright toHaveScreenshot() |
|---|---|---|
| Proces przeglądu w PR | Wbudowany raport i polecenie approve | Ręczna regeneracja snapshotów |
| Zarządzanie wzorcami | Polecenia reference / test / approve | Pliki snapshotów per test |
| Renderowanie znormalizowane przez Dockera | Flaga --docker | Przynieś swój własny obraz |
| Narzut instalacyjny | Osobna zależność | Już obecne, jeśli używasz Playwright |
Jeśli chcesz głębszego starcia obejmującego więcej narzędzi i kryteriów, nasze porównanie BackstopJS, Argos i Lost Pixel omawia cały proces wyboru.
Wdrożenie własnego runnera CI na VPS-ie
Elementem, który faktycznie zapewnia spójność renderowania, jest executor Docker: runner uruchamia każde zadanie w kontenerze, który sam definiujesz, więc środowisko przeglądarki jest za każdym razem identyczne. Platformę runnera dobierz według tego, co jeszcze ma dla ciebie robić.
Samodzielnie hostowany GitLab CE to cięższa opcja: repozytoria, CI/CD, rejestr, zgłoszenia i merge requesty w jednej instancji. Dla tego przewodnika najważniejszy jest executor Docker w GitLab Runner, dzięki któremu każde zadanie regresji wizualnej działa za każdym razem w tym samym przypiętym obrazie kontenera. GitLab, Gitea, Jenkins, Forgejo, Portainer i Docker są dostępne jako wdrożenia jednym kliknięciem w marketplace Cloudzy, co znacznie skraca konfigurację.
Gitea z act-runner to lekka alternatywa. Gitea to napisany w Go serwis Git z silnikiem workflow zgodnym z GitHub Actions (przez act-runner), więc jeśli twój zespół dobrze czuje się w składni Actions, postawisz tu mały i szybki runner.
Cokolwiek wybierzesz, cel konfiguracji jest ten sam: runner, który przyjmuje zadania i wykonuje je za pomocą executora Docker. Wszystko dalej (konfiguracja kontenera, BackstopJS, przykładowy pipeline) zakłada, że ten executor już działa.
Konfiguracja Dockera, żeby Chromium przestało padać
Oto awaria, która kosztuje ludzi najwięcej czasu. Docker domyślnie montuje /dev/shm z rozmiarem 64MB. Headless Chromium używa tej partycji pamięci współdzielonej dla procesów renderujących, a przy zrzucie całej strony potrzebuje dużo więcej niż 64MB. Gdy jej zabraknie, renderer umiera, często bez komunikatu wskazującego na pamięć współdzieloną. Zadanie zawisa, przekracza limit czasu albo zgłasza ogólnikowy błąd przeglądarki.
Są dwa rozwiązania i każde z nich działa.
Rozwiązanie A: zwiększ rozmiar pamięci współdzielonej w Compose. Klucz shm_size ustawia rozmiar partycji /dev/shm w kontenerze. Dokumentacja pliku Docker Compose od Dockera potwierdza, że shm_size konfiguruje rozmiar pamięci współdzielonej dopuszczalnej dla kontenera usługi. Ustaw go w bloku usługi:
# docker-compose.yml
services:
vrt:
image: backstopjs/backstopjs:6.3.25
shm_size: '2gb' # override the 64MB default
volumes:
- ./:/src
working_dir: /src
Rozwiązanie B: każ Chromium w ogóle nie używać /dev/shm. Flaga --disable-dev-shm-usage sprawia, że Chromium zapisuje pliki pamięci współdzielonej do /tmp zamiast na partycję pamięci współdzielonej. Przekaż ją w argumentach uruchomieniowych przeglądarki. W konfiguracji backstop.json to engineOptions, a w Playwright launchOptions:
// backstop.json (fragment)
{
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
Rozwiązanie A jest czystsze, gdy masz kontrolę nad plikiem Compose. Rozwiązanie B jest przenośne, gdy możesz zmieniać tylko flagi uruchomieniowe przeglądarki. Zastosowanie obu niczemu nie szkodzi.
Wskazówka: Używaj dokładnie tego samego obrazu Docker lokalnie i w CI. BackstopJS daje ci to za darmo dzięki fladze --docker, która wykonuje przechwytywanie w przypiętym obrazie referencyjnym, więc twoja maszyna i runner renderują identycznie. To właśnie eliminuje fałszywe alarmy związane z czcionkami i antyaliasingiem: przypnij obraz raz i przestań gonić widmowe różnice.

Konfiguracja BackstopJS pod CI
BackstopJS generuje raport JUnit XML, który runner CI czyta, żeby zaliczyć albo odrzucić build. To cały sens wpinania go w pipeline zamiast uruchamiania ręcznie. Zainstaluj go i zainicjuj konfigurację:
npm install --save-dev backstopjs
npx backstop init
Następnie wskaż w backstop.json strony, które chcesz przechwytywać, i włącz raport CI:
// backstop.json (fragment)
{
"id": "my_app_vrt",
"viewports": [
{ "label": "desktop", "width": 1440, "height": 900 }
],
"scenarios": [
{
"label": "Homepage",
"url": "http://app:3000/",
"selectors": ["document"]
}
],
"report": ["CI"],
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
To ustawienie raportu CI odpowiada za generowanie JUnit XML. Używaj flagi --docker, gdy uruchamiasz te polecenia lokalnie ze swojej maszyny. W zadaniu CI, które już korzysta z obrazu backstopjs/backstopjs, uruchamiaj po prostu backstop test. Praca z wzorcami sprowadza się do trzech poleceń:
npx backstop reference --docker # capture the approved baseline
npx backstop test --docker # run the comparison, emit the report
npx backstop approve # accept the current diffs as the new baseline
Jedno zastrzeżenie co do zarządzania wzorcami: każda uprawniona zmiana UI oznacza uruchomienie approve, żeby zatwierdzić nowe zrzuty. Powyżej mniej więcej stu scenariuszy ten krok akceptacji to realny koszt operacyjny: ktoś musi obejrzeć różnice i zdecydować, które są zamierzone, a obciążenie rośnie wraz z liczbą osób w zespole. To utrzymanie jest faktyczną ceną wizualnych testów regresji na skalę i warto je uwzględnić, zanim zaangażujesz się w duży zestaw testów.
Działająca konfiguracja pipeline'u CI
Pipeline działa w dwóch logicznych częściach: raz (albo na żądanie) generuje wzorzec, a potem porównuje z nim każdą zmianę. Poniższy przykład to konfiguracja GitLab CI z executorem Docker, z poprawką pamięci współdzielonej już zastosowaną przez flagi uruchomieniowe w backstop.json.
# .gitlab-ci.yml
stages:
- visual-regression
visual_regression:
stage: visual-regression
image:
name: backstopjs/backstopjs:6.3.25
entrypoint: [""]
script:
- backstop test
artifacts:
when: always
reports:
junit: backstop_data/ci_report/xunit.xml
paths:
- backstop_data/bitmaps_test
- backstop_data/html_report
expire_in: 1 week
Linia junit przekazuje raport do GitLaba, dzięki czemu błędy pojawiają się w interfejsie merge requestu. Linia paths zachowuje bitmapy różnic, żebyś mógł zobaczyć, co faktycznie się zmieniło. W Gitea z act-runner te same kroki przekładają się na workflow w stylu Actions:
# .gitea/workflows/visual-regression.yaml
name: Visual Regression
on: [push, pull_request]
jobs:
vrt:
runs-on: ubuntu-latest
container:
image: backstopjs/backstopjs:6.3.25
options: --shm-size=2gb # raise /dev/shm for the job container
steps:
- uses: actions/checkout@v4
- run: backstop test
Zwróć uwagę na opcję kontenera --shm-size=2gb w workflow dla Gitea. To ta sama poprawka pamięci współdzielonej co w rozwiązaniu A, zastosowana na poziomie kontenera zadania, gdzie nie ma osobnego pliku Compose do edycji.
Dobór rozmiaru VPS-a pod headless Chromium
Wąskim gardłem jest tu RAM, nie CPU. Zużycie CPU przez headless Chromium jest skokowe (rośnie podczas przechwytywania i spada pomiędzy), więc skromna liczba rdzeni spokojnie wystarczy. To pamięć jest sufitem, który decyduje, ile zadań uruchomisz naraz. Proces headless Chromium zajmuje w spoczynku około 300–500MB i rośnie do 1–2GB podczas przechwytywania zrzutu całej strony, więc o rozmiarze decyduje RAM na jedno równoległe zadanie.
| RAM | Odpowiednie dla |
|---|---|
| 2GB | Ciasne minimum: jedno zadanie sekwencyjne |
| 4GB | Zalecana baza: 1–2 zadania równoległe |
| 8GB | 3–4 zadania równoległe |
| 16GB | Duże zestawy testów i równoległe pipeline'y |
Te liczby to wskazówki praktyków wyciągnięte z prawdziwych uruchomień CI, a nie benchmark producenta, więc traktuj je jako przybliżony punkt wyjścia i obserwuj własne szczyty zużycia pamięci.
Najważniejszy wniosek z tej sekcji: 4GB RAM to niezawodna baza, a na każde dodatkowe równoległe zadanie regresji wizualnej dołóż około 2GB zapasu.
Haczyk z samodzielnie hostowanym VRT polega na tym, że runner z niedoborem pamięci nie zgłasza głośnego błędu, tylko po cichu wywala Chromium, czyli dokładnie ten objaw, przed którym cała ta konfiguracja ma chronić. Zapewnienie odpowiedniego zapasu pamięci to najtańsze ubezpieczenie, jakie możesz kupić. Jeśli wolisz nie budować hosta dla runnera od zera, marketplace Cloudzy ma wdrożenia jednym kliknięciem dla GitLab na własnym serwerze oraz Gitea na własnym serwerze które dają działającą instancję CI w kilka minut, na VPS-ie dobranym dokładnie pod zapas pamięci dla headless Chromium, jakiego wymaga twój zestaw testów.

Często zadawane pytania
Jak skonfigurować wizualne testy regresji w pipelinie CI/CD?
Zainstaluj BackstopJS, wskaż w backstop.json docelowe strony i ustaw opcję raportu CI tak, żeby generowała JUnit XML. Uruchamiając z lokalnej maszyny, używaj backstop reference --docker i backstop test --docker, żeby zrzuty renderowały się w przypiętym obrazie Docker. W CI uruchamiaj zadanie w obrazie backstopjs/backstopjs i wywołuj po prostu backstop test, a następnie publikuj raport JUnit z backstop_data/ci_report/xunit.xml. Użyj samodzielnie hostowanego runnera z executorem Docker, z co najmniej 4GB RAM dla headless Chromium.
Ile RAM potrzebuje headless Chromium w kontenerze CI?
Proces headless Chromium zużywa w spoczynku około 300–500MB i skacze do 1–2GB podczas przechwytywania zrzutu całej strony. Dla niezawodnego CI zaplanuj około 4GB RAM na jedno do dwóch równoległych zadań i dokładaj mniej więcej 2GB na każde kolejne. CPU pracuje skokowo i nie jest wąskim gardłem. To RAM decyduje, ile zadań uruchomisz jednocześnie.
Dlaczego moje wizualne testy regresji przechodzą lokalnie, a padają w CI?
Renderowanie w przeglądarce różni się między środowiskami: czcionki, antyaliasing, tryb headless kontra z interfejsem oraz OS zmieniają wynik. Rozwiązaniem jest generowanie wzorców i porównywanie w tym samym obrazie Docker, żeby oba renderowały identycznie. Flaga --docker w BackstopJS robi dokładnie to, wykonując przechwytywanie w przypiętym obrazie referencyjnym.
Jak naprawić błąd /dev/shm w Chromium w Dockerze na CI?
Domyślny /dev/shm w Dockerze ma 64MB, co jest zbyt małe dla renderera headless Chromium i powoduje ciche awarie. Zwiększ go przez shm_size: '2gb' w docker-compose.yml (albo --shm-size=2gb na kontenerze zadania) lub przekaż --disable-dev-shm-usage we flagach uruchomieniowych Chromium, żeby zapisywało pliki pamięci współdzielonej do /tmp.
Wybrać Playwright toHaveScreenshot czy dedykowane narzędzie do regresji wizualnej?
Wbudowane toHaveScreenshot() w Playwright wystarcza dla małych zestawów testów: poniżej mniej więcej pięćdziesięciu ekranów, bez współdzielonej biblioteki komponentów i bez potrzeby przeglądu w PR. Przejdź na dedykowane narzędzie w rodzaju BackstopJS, gdy potrzebujesz porządnego procesu zatwierdzania wzorców, raportu do przeglądu i renderowania znormalizowanego przez Dockera, co zaczyna mieć znaczenie, gdy liczba wzorców rośnie.
Czy Lost Pixel jest nadal utrzymywany?
Nie. Lost Pixel został zarchiwizowany w kwietniu 2026 roku i nie jest kandydatem do nowych wdrożeń. Do samodzielnie hostowanych wizualnych testów regresji użyj BackstopJS.