На вашому ноутбуці візуальні регресійні тести проходять. Ви робите push, конвеєр запускається — і ті самі тести або мовчки кладуть браузер, або позначають п'ятдесят «змін», які насправді є лише різницею в згладжуванні. У коді нічого не змінилося, але CI має іншу думку.
Ця прірва між «працює локально» і «працює в CI» майже завжди є проблемою інфраструктури, а не тестів. Щоб надійно розгорнути власне візуальне регресійне тестування в CI-конвеєрі, потрібно правильно з'єднати чотири частини: CI-раннер з Docker-екзекутором, контейнер, налаштований так, щоб headless Chromium не залишався без спільної пам'яті, інструмент порівняння, який видає звіти, зрозумілі для CI, і VPS, підібраний під ту пам'ять, яку headless-браузери справді споживають.
Цей посібник збирає такий стек від початку до кінця. Наприкінці у вас буде робочий .gitlab-ci.yml (або еквівалент для Gitea), середовище Docker, у якому Chromium не падає, BackstopJS, що видає звіти JUnit, зрозумілі вашому конвеєру, і правильно підібраний VPS, щоб усе це запустити.
TL;DR
- Запускайте браузер в одному й тому самому образі Docker локально та в CI. Більшість хибнопозитивних відмінностей виникає через різницю шрифтів і рендерингу між середовищами. Зафіксуйте образ — і вони зникнуть.
- Виправте типове значення /dev/shm у 64MB. Розмір спільної пам’яті за замовчуванням у Docker не залишає ресурсів рендерер-процесу Chromium, і той мовчки падає в CI. Задайте shm_size: '2gb' у docker-compose.yml або передайте Chromium прапорець --disable-dev-shm-usage.
- Використовуйте BackstopJS як основний інструмент для власного розгортання. Він має ліцензію MIT (v6.3.25), орієнтований на Docker завдяки прапорцю --docker і видає звіти JUnit. Якщо ви вже користуєтеся Playwright, його вбудований toHaveScreenshot() — цілком пристойний старт без жодних встановлень.
- Беріть 4GB RAM за базовий орієнтир. Закладайте пам'ять із запасом. На практиці в CI задача зі знімками екрана в Chromium під час захоплення повної сторінки споживає значно більше пам'яті, ніж здається за станом простою, тож 4GB RAM — безпечніший орієнтир для однієї-двох послідовних або злегка паралельних задач.
Що цей посібник не охоплює
Це посібник зі складання інфраструктури, яку ви вже вирішили розгорнути. Кілька речей свідомо залишено поза межами:
- Це не повне порівняння інструментів. Посібник зосереджений на стеку, який реально обслуговувати у власному CI-середовищі: BackstopJS, Docker і ваш власний раннер.
- Він не охоплює власне розгортання Argos. Argos має відкритий код, але його публічний продуктовий сценарій і документація зосереджені на хмарному застосунку Argos та CI-інтеграціях, а не на простому шляху самостійного розгортання у продакшені. Для мети цього посібника BackstopJS — надійніший робочий приклад.
- Він не рекомендує Lost Pixel. Цей проєкт заархівовано у квітні 2026 року; для нових розгортань використовуйте BackstopJS.
- Він не переповідає суперечку «власний хостинг проти керованого SaaS». Якщо ви тут, то Percy чи Chromatic ви вже відкинули.
Передумови
Перед першою командою знадобиться кілька речей:
- Власний CI-раннер або план його розгорнути (про це нижче).
- Docker, встановлений на хості раннера.
- Проєкт на Node.js із наявною ціллю для тестування (URL запущеного застосунку або набір маршрутів компонентів для захоплення).
- Доступ до оболонки на VPS чи хості, де живе раннер.
Чому візуальні тести проходять локально, але ламаються в CI
Браузер, який рендерить ваш еталон на macOS, — це не той браузер, що рендерить порівняння в Linux-контейнері CI. Документація самого Playwright говорить про це прямо: рендеринг у браузері може відрізнятися залежно від OS хоста, версії, налаштувань, апаратного забезпечення, джерела живлення, headless-режиму та інших чинників. Рекомендація так само пряма: щоб знімки екрана були стабільними, запускайте тести в тому самому середовищі, де були згенеровані еталонні знімки екрана.
Саме ця рекомендація і є причиною всього подальшого налаштування. Майже за кожним хибним падінням стоїть одна з трьох невідповідностей:
- Різниця у шрифтах і згладжуванні. На вашій робочій машині й у контейнері CI різні пакети шрифтів і різний субпіксельний рендеринг. Сторінки з великою кількістю тексту дають відмінності на кожному запуску, навіть коли нічого не змінювалося.
- Headless-рендеринг проти рендерингу з інтерфейсом. Браузер з інтерфейсом і headless-браузер можуть трохи по-різному будувати макет однієї й тієї самої сторінки.
- Вичерпання спільної пам'яті. Це тиха проблема. Типовий /dev/shm у Docker — 64MB, а headless Chromium використовує спільну пам'ять для своїх рендерер-процесів. Коли вона вичерпується, Chromium падає без жодної корисної помилки. Ваша задача просто вмирає або зависає.
Перші дві проблеми вирішує фіксація одного образу Docker і для генерації еталона, і для порівняння. Третю розв'язано в розділі про конфігурацію Docker нижче. Виправте всі три — і тести, що були «нестабільними в CI», стануть детермінованими.

Вибір інструмента: BackstopJS чи вбудоване порівняння Playwright
Якщо ви вже використовуєте Playwright для наскрізних тестів, найшвидший шлях — його вбудоване твердження toHaveScreenshot(): жодних додаткових залежностей, попіксельне порівняння та еталони з суфіксом платформи з коробки. Для невеликого набору тестів це цілком прийнятна зупинка, а командам, у яких приблизно менше п'ятдесяти екранів і немає спільної бібліотеки компонентів, більшого може ніколи й не знадобитися.
Дорога закінчується там, де починається процес рецензування. Порівняння в Playwright — суто попіксельне, без UI для перегляду в PR, тож затвердити правомірну партію візуальних змін означає перегенерувати знімки вручну. Коли кількість еталонів зростає, цей ручний цикл затвердження стає болючим.
BackstopJS — спеціалізований інструмент, створений саме для цього циклу. Він має ліцензію MIT (наразі v6.3.25), орієнтований на Docker через прапорець --docker і видає JUnit XML, який CI-раннер читає нативно. Його робота з еталонами (згенерувати референс, протестувати проти нього, затвердити прийняті відмінності як новий еталон) — саме та операційна модель, до якої зрештою приходить більшість власних VRT-конфігурацій. У решті посібника BackstopJS буде робочим прикладом.
Два інструменти, які вам можуть радити деінде, у цьому розборі не розглядаються: Argos — тому що його публічний продуктовий сценарій зосереджений на хмарному застосунку та CI-інтеграціях, а Lost Pixel заархівовано. Для практичного власного розгортання BackstopJS — надійніший робочий приклад.
| Вісь | BackstopJS | Playwright toHaveScreenshot() |
|---|---|---|
| Процес рецензування в PR | Вбудований звіт і команда approve | Ручна перегенерація знімків |
| Керування еталонами | Команди reference / test / approve | Файли знімків для кожного тесту |
| Рендеринг, нормалізований через Docker | прапорець --docker | Принесіть свій образ |
| Витрати на встановлення | Окрема залежність | Уже є, якщо ви користуєтеся Playwright |
Для глибшого зіставлення більшої кількості інструментів і критеріїв наше порівняння BackstopJS, Argos і Lost Pixel розглядає вибір повністю.
Розгортання власного CI-раннера на VPS
Стабільність рендерингу насправді забезпечує Docker-екзекутор: раннер виконує кожну задачу всередині контейнера, який визначаєте ви, тож середовище браузера щоразу однакове. Платформу раннера обирайте за тим, що ще ви хочете на неї покласти.
Власний GitLab CE — важчий варіант: репозиторії, CI/CD, реєстр, задачі та запити на злиття в одному екземплярі. Для цього посібника важливий Docker-екзекутор GitLab Runner, який дає змогу щоразу виконувати кожну задачу візуальної регресії всередині одного й того самого зафіксованого образу контейнера. GitLab, Gitea, Jenkins, Forgejo, Portainer і Docker доступні як розгортання в один клік у маркетплейс Cloudzy, що суттєво скорочує налаштування.
Gitea з act-runner — легка альтернатива. Gitea — це Git-сервіс на Go з рушієм робочих процесів, сумісним із GitHub Actions (через act-runner), тож якщо ваша команда звикла до синтаксису Actions, це компактний і швидкий у розгортанні раннер.
Що б ви не обрали, мета конфігурації однакова: раннер, який приймає задачі й виконує їх через Docker-екзекутор. Усе подальше (конфігурація контейнера, BackstopJS, зразок конвеєра) виходить із того, що цей екзекутор уже на місці.
Налаштування Docker, щоб Chromium не падав
Ось збій, що коштує людям найбільше часу. За замовчуванням Docker монтує /dev/shm розміром 64MB. Headless Chromium використовує цей розділ спільної пам'яті для своїх рендерер-процесів, а під час знімка повної сторінки йому потрібно значно більше за 64MB. Коли пам'ять вичерпується, рендерер помирає, часто без жодного повідомлення, що вказувало б на спільну пам'ять. Задача зависає, вилітає за тайм-аутом або повідомляє про загальний збій браузера.
Є два виправлення, і працює будь-яке з них.
Виправлення A: збільшіть розмір спільної пам'яті в Compose. Ключ shm_size задає розмір розділу /dev/shm усередині контейнера. довідник із файлу Docker Compose від Docker підтверджує, що shm_size налаштовує обсяг спільної пам'яті, дозволений контейнеру сервісу. Задайте його в блоці сервісу:
# docker-compose.yml
services:
vrt:
image: backstopjs/backstopjs:6.3.25
shm_size: '2gb' # override the 64MB default
volumes:
- ./:/src
working_dir: /src
Виправлення B: скажіть Chromium взагалі не використовувати /dev/shm. Прапорець --disable-dev-shm-usage змушує Chromium писати файли спільної пам'яті в /tmp замість розділу спільної пам'яті. Передайте його в аргументах запуску браузера. У конфігурації backstop.json це engineOptions; у Playwright — launchOptions:
// backstop.json (fragment)
{
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
Виправлення A акуратніше, коли ви керуєте файлом Compose; виправлення B — переносний варіант, коли ви можете змінювати лише прапорці запуску браузера. Застосувати обидва не зашкодить.
Порада: Використовуйте абсолютно однаковий образ Docker локально та в CI. BackstopJS дає це задарма через прапорець --docker, який виконує захоплення всередині зафіксованого еталонного образу, тож ваша машина і раннер рендерять однаково. Саме це усуває хибнопозитивні спрацювання через шрифти та згладжування: зафіксуйте образ один раз і припиніть ганятися за примарними відмінностями.

Налаштування BackstopJS для CI
BackstopJS створює звіт JUnit XML, за яким ваш CI-раннер вирішує, пройшла збірка чи ні, — у цьому і весь сенс вбудовувати його в конвеєр, а не запускати вручну. Встановіть його та ініціалізуйте конфігурацію:
npm install --save-dev backstopjs
npx backstop init
Далі вкажіть у backstop.json сторінки, які потрібно захоплювати, і ввімкніть звіт для 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"]
}
}
Саме налаштування звіту для CI видає JUnit XML. Використовуйте прапорець --docker, коли запускаєте ці команди локально зі своєї хост-машини. Усередині CI-задачі, яка вже використовує образ backstopjs/backstopjs, запускайте backstop test напряму. Робота з еталонами зводиться до трьох команд:
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
Одне застереження щодо еталонів: кожна правомірна зміна UI означає запуск approve, щоб благословити нові знімки. Після приблизно сотні сценаріїв цей крок затвердження стає реальною операційною витратою: хтось має переглянути відмінності очима й вирішити, які з них навмисні, і це навантаження зростає разом із командою. Таке обслуговування — справжня ціна візуального регресійного тестування у великому масштабі, і її варто закласти в бюджет, перш ніж заводити великий набір тестів.
Робоча конфігурація CI-конвеєра
Конвеєр працює у дві логічні частини: один раз (або за потреби) згенерувати еталон, а потім порівнювати з ним на кожній зміні. Нижче наведено конфігурацію GitLab CI з Docker-екзекутором, у якій виправлення спільної пам'яті вже застосовано через прапорці запуску в 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
Рядок junit передає звіт до GitLab, щоб падіння з'являлися в інтерфейсі запиту на злиття; рядок paths зберігає растрові зображення відмінностей, щоб ви могли подивитися, що саме змінилося. У Gitea з act-runner ті самі кроки перекладаються на робочий процес у стилі 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
Зверніть увагу на опцію контейнера --shm-size=2gb у робочому процесі Gitea. Це те саме виправлення спільної пам'яті, що й виправлення A, застосоване на рівні контейнера задачі, де немає окремого файлу Compose для редагування.
Підбір VPS під headless Chromium
Обмеженням тут є RAM, а не CPU. Навантаження на CPU в headless Chromium стрибкоподібне (пік під час захоплення і простій між ними), тож скромної кількості ядер цілком вистачає. Стелею, яка визначає, скільки задач можна виконувати одночасно, є пам'ять. Процес headless Chromium у простої тримається приблизно на 300–500MB і піднімається до 1–2GB під час знімка повної сторінки, тож ключовий чинник підбору — обсяг RAM на одну паралельну задачу.
| RAM | Підходить для |
|---|---|
| 2GB | Жорсткий мінімум: одна послідовна задача |
| 4GB | Рекомендований орієнтир: 1–2 паралельні задачі |
| 8GB | 3–4 паралельні задачі |
| 16GB | Великі набори тестів і паралельні конвеєри |
Ці цифри — практичні орієнтири з реальних запусків CI, а не вендорський бенчмарк, тож сприймайте їх як приблизну відправну точку і стежте за власними піками пам'яті.
Головне з розділу: 4GB RAM — надійний орієнтир; додавайте приблизно 2GB запасу на кожну додаткову паралельну задачу візуальної регресії.
Підступ самостійно розміщеного VRT у тому, що ранер, якому бракує пам’яті, не падає з гучною помилкою — він тихо кладе Chromium, а це саме той симптом, заради уникнення якого вся ця конструкція й існує. Закласти достатній запас пам’яті — найдешевша страховка, яку можна купити. Якщо збирати хост для ранера з нуля не хочеться, у маркетплейсі Cloudzy є розгортання в один клік для GitLab на власному сервері та Gitea на власному сервері , які за кілька хвилин дають робочий екземпляр CI на VPS, який ви можете підібрати рівно під той запас для headless Chromium, якого потребує ваш набір тестів.

Часті запитання
Як налаштувати візуальне регресійне тестування у CI/CD-конвеєрі?
Встановіть BackstopJS, вкажіть у backstop.json цільові сторінки та ввімкніть опцію звіту для CI, щоб отримувати JUnit XML. Запускаючи з локальної машини, використовуйте backstop reference --docker і backstop test --docker, щоб знімки рендерилися всередині зафіксованого образу Docker. У CI виконуйте задачу всередині образу backstopjs/backstopjs і викликайте backstop test напряму, а потім публікуйте звіт JUnit із backstop_data/ci_report/xunit.xml. Використовуйте власний раннер із Docker-екзекутором, розрахований щонайменше на 4GB RAM для headless Chromium.
Скільки RAM потрібно headless Chromium у CI-контейнері?
Процес headless Chromium у простої споживає приблизно 300–500MB і піднімається до 1–2GB під час знімка повної сторінки. Для надійного CI розраховуйте приблизно на 4GB RAM на одну-дві паралельні задачі, додаючи близько 2GB на кожну наступну паралельну задачу. Навантаження на CPU стрибкоподібне і обмеженням не є; саме RAM визначає, скільки задач можна виконувати одночасно.
Чому мої візуальні регресійні тести проходять локально, але падають у CI?
Рендеринг у браузері відрізняється між середовищами: шрифти, згладжування, headless-режим проти режиму з інтерфейсом і сама OS змінюють результат. Виправлення — виконувати генерацію еталона й порівняння в одному образі Docker, щоб обидва рендерилися однаково. Прапорець --docker у BackstopJS робить це, виконуючи захоплення всередині зафіксованого еталонного образу.
Як виправити помилку /dev/shm у Chromium в Docker-CI?
Типовий /dev/shm у Docker має 64MB, що замало для рендерера headless Chromium і призводить до тихих падінь. Або збільште його через shm_size: '2gb' у docker-compose.yml (чи --shm-size=2gb на контейнері задачі), або передайте --disable-dev-shm-usage у прапорцях запуску Chromium, щоб він писав файли спільної пам'яті в /tmp.
Що обрати: Playwright toHaveScreenshot чи спеціалізований інструмент візуальної регресії?
Вбудованого toHaveScreenshot() у Playwright вистачає для невеликих наборів: приблизно до п'ятдесяти екранів, без спільної бібліотеки компонентів і без потреби в UI для перегляду в PR. Переходьте на спеціалізований інструмент на кшталт BackstopJS, коли потрібен повноцінний процес затвердження еталонів, звіт для рецензування та рендеринг, нормалізований через Docker, — це стає важливим, щойно кількість еталонів зростає.
Чи підтримується Lost Pixel досі?
Ні. Lost Pixel заархівовано у квітні 2026 року, і для нових розгортань він не підходить. Для власного візуального регресійного тестування використовуйте BackstopJS.