Tes visual regression Anda lolos di laptop. Anda push, pipeline berjalan, dan tes yang sama justru membuat browser crash tanpa pesan atau menandai lima puluh "perubahan" yang sebenarnya cuma beda anti-aliasing. Tidak ada satu baris kode pun yang berubah, tapi CI berpendapat lain.
Jarak antara "jalan di lokal" dan "jalan di CI" hampir selalu masalah infrastruktur, bukan masalah tes. Untuk menjalankan visual regression testing sendiri di pipeline CI secara andal, Anda butuh empat bagian yang tersambung dengan benar: runner CI dengan Docker executor, container yang dikonfigurasi agar headless Chromium tidak kehabisan shared memory, tool diffing yang menghasilkan laporan yang bisa dibaca CI, dan VPS dengan memori yang sesuai konsumsi nyata browser headless.
Panduan ini membangun stack tersebut dari ujung ke ujung. Di akhir Anda akan punya .gitlab-ci.yml yang berfungsi (atau padanannya di Gitea), lingkungan Docker yang mencegah Chromium crash, BackstopJS yang menghasilkan laporan JUnit yang bisa dibaca pipeline Anda, dan VPS dengan ukuran yang pas untuk menjalankan semuanya.
TL;DR
- Jalankan browser di image Docker yang sama, baik di lokal maupun di CI. Sebagian besar diff false positive berasal dari perbedaan font dan rendering antar lingkungan. Pin image-nya dan perbedaan itu hilang.
- Perbaiki nilai default /dev/shm sebesar 64MB. ukuran shared memory bawaan Docker sehingga renderer Chromium kehabisan memori dan crash tanpa pesan di CI. Setel shm_size: '2gb' di docker-compose.yml, atau berikan --disable-dev-shm-usage ke Chromium.
- Gunakan BackstopJS sebagai tool utama yang bisa di-self-host. Lisensinya MIT (v6.3.25), Docker-first lewat flag --docker, dan menghasilkan laporan JUnit. Jika Anda sudah memakai Playwright, toHaveScreenshot() bawaannya adalah titik awal yang bagus tanpa instalasi tambahan.
- Ambil 4GB RAM sebagai patokan dasar. Alokasikan memori dengan longgar. Dalam eksekusi CI sehari-hari, job screenshot Chromium bisa memakai memori jauh lebih besar saat menangkap halaman penuh dibanding kebutuhannya saat idle, jadi 4GB RAM adalah patokan yang lebih aman untuk satu sampai dua job sekuensial atau paralel ringan.
Apa yang Tidak Dicakup Panduan Ini
Ini panduan membangun infrastruktur yang sudah Anda putuskan untuk dipasang. Beberapa hal sengaja berada di luar cakupan:
- Ini bukan perbandingan tool yang lengkap. Panduan ini fokus pada stack yang benar-benar bisa Anda operasikan di lingkungan CI self-hosted: BackstopJS, Docker, dan runner milik Anda sendiri.
- Panduan ini tidak membahas cara self-host Argos. Argos memang open source, tapi alur produk dan dokumentasi publiknya berfokus pada aplikasi Argos versi hosted dan integrasi CI, bukan pada jalur self-hosting produksi yang sederhana. Untuk tujuan panduan ini, BackstopJS adalah contoh kerja yang lebih aman.
- Panduan ini tidak merekomendasikan Lost Pixel. Proyek itu diarsipkan pada April 2026; gunakan BackstopJS untuk deployment baru.
- Panduan ini tidak mengulang debat self-hosting versus SaaS terkelola. Kalau Anda ada di sini, Anda sudah mencoret Percy atau Chromatic.
Prasyarat
Ada beberapa hal yang harus siap sebelum perintah pertama:
- Runner CI self-hosted, atau rencana untuk men-deploy-nya (dibahas di bawah).
- Docker terpasang di host runner.
- Proyek Node.js dengan target tes yang sudah ada (URL aplikasi yang berjalan atau sekumpulan route komponen untuk ditangkap).
- Akses shell ke VPS atau host tempat runner berada.
Kenapa tes visual lolos di lokal tapi gagal di CI
Browser yang merender baseline Anda di macOS bukanlah browser yang merender pembandingnya di container CI Linux. Dokumentasi resmi Playwright terus terang soal ini: rendering browser bisa berbeda tergantung OS host, versi, pengaturan, hardware, sumber daya listrik, mode headless, dan faktor lain. Rekomendasinya sama tegasnya: untuk screenshot yang konsisten, jalankan tes di lingkungan yang sama dengan tempat screenshot baseline dibuat.
Rekomendasi tunggal itulah alasan seluruh setup berikutnya ada. Tiga ketidakcocokan menjelaskan hampir semua kegagalan palsu:
- Perbedaan font dan anti-aliasing. Mesin dev Anda dan container CI membawa paket font dan rendering sub-pixel yang berbeda. Halaman padat teks akan menghasilkan diff di setiap eksekusi meski tidak ada yang berubah.
- Rendering headless versus headed. Browser headed dan headless bisa menata halaman yang sama dengan sedikit perbedaan.
- Shared memory yang habis. Ini penyebab yang diam-diam. Nilai default /dev/shm di Docker adalah 64MB, sementara headless Chromium memakai shared memory untuk proses renderer-nya. Begitu habis, Chromium crash tanpa pesan error yang berguna. Job Anda tiba-tiba mati atau menggantung.
Dua penyebab pertama diselesaikan dengan mem-pin satu image Docker untuk pembuatan baseline sekaligus pembandingannya. Penyebab ketiga diselesaikan di bagian konfigurasi Docker di bawah. Perbaiki ketiganya dan tes yang tadinya "flaky di CI" jadi deterministik.

Memilih tool: BackstopJS atau diffing bawaan Playwright
Kalau Anda sudah memakai Playwright untuk tes end-to-end, jalur tercepat adalah assertion toHaveScreenshot() bawaannya: tanpa dependensi tambahan, diffing tingkat pixel, dan baseline bersufiks platform langsung dari kotaknya. Ini titik henti yang sah untuk suite kecil, dan bagi tim dengan sekitar kurang dari lima puluh layar tanpa library komponen bersama, ini mungkin sudah cukup selamanya.
Batasnya ada di alur review. Diffing Playwright hanya berbasis pixel dan tidak punya UI review PR, jadi menyetujui sekumpulan perubahan visual yang memang disengaja berarti membuat ulang snapshot secara manual. Begitu jumlah baseline Anda bertambah, loop persetujuan manual itu jadi menyiksa.
BackstopJS adalah tool khusus yang dibuat persis untuk loop itu. Lisensinya MIT (saat ini v6.3.25), Docker-first lewat flag --docker, dan menghasilkan JUnit XML yang bisa dibaca runner CI secara native. Alur baseline-nya (buat reference, uji terhadapnya, setujui diff yang diterima sebagai baseline baru) adalah model operasional yang pada akhirnya diinginkan sebagian besar setup VRT self-hosted. Untuk sisa panduan ini, BackstopJS adalah contoh kerjanya.
Dua tool yang mungkin Anda lihat direkomendasikan di tempat lain tidak dipakai di panduan ini: Argos tidak dibahas karena alur produk publiknya berpusat pada aplikasi hosted dan integrasi CI, sementara Lost Pixel sudah diarsipkan. BackstopJS adalah contoh kerja yang lebih aman untuk setup self-hosted yang praktis.
| Sumbu | BackstopJS | Playwright toHaveScreenshot() |
|---|---|---|
| Alur review PR | Laporan bawaan dan perintah approve | Pembuatan ulang snapshot secara manual |
| Manajemen baseline | Perintah reference / test / approve | File snapshot per tes |
| Rendering yang dinormalkan lewat Docker | Flag --docker | Bawa image sendiri |
| Beban instalasi | Dependensi terpisah | Sudah ada jika Anda memakai Playwright |
Untuk adu banding yang lebih dalam dengan lebih banyak tool dan sudut penilaian, perbandingan BackstopJS, Argos, dan Lost Pixel dari kami membahas keputusan pemilihannya secara lengkap.
Men-deploy runner CI self-hosted di VPS
Bagian yang benar-benar memberi konsistensi rendering adalah Docker executor: runner menjalankan setiap job di dalam container yang Anda tentukan, artinya lingkungan browser selalu identik. Pilih platform runner berdasarkan seberapa banyak tugas lain yang Anda ingin dia tangani.
GitLab CE self-hosted adalah opsi yang lebih berat: repositori, CI/CD, registry, issue, dan merge request dalam satu instance. Bagian yang penting untuk panduan ini adalah Docker executor milik GitLab Runner, yang membuat setiap job visual regression berjalan di dalam image container ter-pin yang sama setiap kali. GitLab, Gitea, Jenkins, Forgejo, Portainer, dan Docker semuanya tersedia sebagai deployment sekali klik di marketplace Cloudzy, yang memangkas waktu setup secara signifikan.
Gitea dengan act-runner adalah alternatif yang ringan. Gitea adalah layanan Git berbasis Go dengan engine workflow yang kompatibel dengan GitHub Actions (lewat act-runner), jadi kalau tim Anda sudah terbiasa dengan sintaks Actions, ini runner kecil yang cepat disiapkan.
Apa pun pilihan Anda, tujuan konfigurasinya sama: runner yang menerima job dan menjalankannya dengan Docker executor. Semua langkah berikutnya (konfigurasi container, BackstopJS, contoh pipeline) mengasumsikan executor itu sudah terpasang.
Mengonfigurasi Docker supaya Chromium tidak crash
Inilah kegagalan yang paling banyak menyita waktu orang. Docker me-mount /dev/shm sebesar 64MB secara default. Headless Chromium memakai partisi shared memory itu untuk proses renderer-nya, dan saat mengambil screenshot halaman penuh dia butuh jauh lebih dari 64MB. Begitu habis, renderer mati, sering kali tanpa pesan error yang menunjuk ke shared memory. Job-nya menggantung, timeout, atau melaporkan crash browser yang generik.
Ada dua perbaikan, dan keduanya sama-sama berhasil.
Perbaikan A: naikkan ukuran shared memory di Compose. Kunci shm_size menentukan ukuran partisi /dev/shm milik container. referensi file Docker Compose menegaskan bahwa shm_size mengatur besar shared memory yang diizinkan untuk container layanan tersebut. Setel di blok service:
# docker-compose.yml
services:
vrt:
image: backstopjs/backstopjs:6.3.25
shm_size: '2gb' # override the 64MB default
volumes:
- ./:/src
working_dir: /src
Perbaikan B: minta Chromium untuk tidak memakai /dev/shm sama sekali. Flag --disable-dev-shm-usage membuat Chromium menulis file shared memory ke /tmp, bukan ke partisi shared memory. Berikan flag itu di argumen peluncuran browser Anda. Di konfigurasi backstop.json, tempatnya di engineOptions; di Playwright, di launchOptions:
// backstop.json (fragment)
{
"engine": "puppeteer",
"engineOptions": {
"args": ["--disable-dev-shm-usage", "--no-sandbox"]
}
}
Perbaikan A lebih rapi kalau Anda menguasai file Compose; Perbaikan B pilihan yang portabel kalau Anda hanya bisa menyentuh flag peluncuran browser. Menerapkan keduanya sekaligus tidak berbahaya.
Tips Pro: Pakai image Docker yang persis sama di lokal dan di CI. BackstopJS memberikan ini secara cuma-cuma lewat flag --docker, yang menjalankan proses capture di dalam image reference ter-pin sehingga mesin Anda dan runner merender dengan hasil identik. Inilah yang menghapus false positive akibat font dan anti-aliasing: pin image sekali dan berhenti mengejar diff hantu.

Menyiapkan BackstopJS untuk CI
BackstopJS menghasilkan laporan JUnit XML yang dibaca runner CI Anda untuk meluluskan atau menggagalkan build, dan itulah inti dari memasangnya ke pipeline alih-alih menjalankannya manual. Instal dan inisialisasi konfigurasinya:
npm install --save-dev backstopjs
npx backstop init
Lalu arahkan backstop.json ke halaman yang ingin Anda tangkap dan aktifkan laporan 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"]
}
}
Pengaturan laporan CI itulah yang menghasilkan JUnit XML. Gunakan flag --docker saat menjalankan perintah ini secara lokal dari mesin host Anda. Di dalam job CI yang sudah memakai image backstopjs/backstopjs, jalankan backstop test secara langsung saja. Alur baseline-nya terdiri dari tiga perintah:
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
Satu catatan soal manajemen baseline: setiap perubahan UI yang memang disengaja berarti menjalankan approve untuk mengesahkan screenshot baru. Lewat sekitar seratus skenario, langkah persetujuan itu jadi biaya operasional yang nyata: seseorang harus memelototi diff-nya dan memutuskan mana yang disengaja, dan bebannya ikut membesar seiring tim yang mengadopsinya. Pemeliharaan itulah harga sebenarnya dari visual regression testing berskala besar, dan layak Anda perhitungkan sebelum berkomitmen pada suite yang besar.
Konfigurasi pipeline CI yang berfungsi
Pipeline berjalan dalam dua bagian logis: buat baseline sekali (atau sesuai kebutuhan), lalu bandingkan terhadapnya di setiap perubahan. Contoh di bawah adalah konfigurasi GitLab CI dengan Docker executor, di mana perbaikan shared memory sudah diterapkan lewat flag peluncuran di 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
Baris junit menyerahkan laporan ke GitLab supaya kegagalan muncul di UI merge request; baris paths menyimpan bitmap diff supaya Anda bisa melihat apa yang sebenarnya berubah. Di Gitea dengan act-runner, langkah yang sama dipetakan ke workflow bergaya 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
Perhatikan opsi container --shm-size=2gb di workflow Gitea. Itu perbaikan shared memory yang sama dengan Perbaikan A, hanya diterapkan di level container job karena tidak ada file Compose terpisah untuk diedit.
Menentukan ukuran VPS untuk headless Chromium
Yang jadi batasan di sini adalah RAM, bukan CPU. Pemakaian CPU headless Chromium bersifat bursty (melonjak saat capture dan idle di antaranya), jadi jumlah core yang sedang saja sudah cukup. Memori adalah plafon yang menentukan berapa job yang bisa Anda jalankan bersamaan. Satu proses headless Chromium berada di sekitar 300–500MB saat idle dan naik ke 1–2GB saat menangkap screenshot halaman penuh, jadi penentu ukuran Anda adalah RAM per job bersamaan.
| RAM | Cocok untuk |
|---|---|
| 2GB | Minimum mepet: satu job sekuensial |
| 4GB | Patokan yang disarankan: 1–2 job bersamaan |
| 8GB | 3–4 job bersamaan |
| 16GB | Suite besar dan pipeline paralel |
Angka-angka ini adalah panduan praktisi yang diambil dari eksekusi CI nyata, bukan benchmark vendor, jadi perlakukan sebagai titik awal perkiraan dan pantau puncak pemakaian memori Anda sendiri.
Poin utama bagian ini: 4GB RAM adalah patokan yang andal; tambahkan sekitar 2GB headroom untuk setiap job visual regression bersamaan berikutnya.
Masalahnya dengan VRT yang di-host sendiri adalah runner yang kekurangan memori tidak gagal dengan gaduh, ia membuat Chromium crash tanpa suara, persis gejala yang ingin dihindari oleh seluruh setup ini. Menyediakan headroom yang cukup adalah asuransi termurah yang bisa Anda beli. Kalau Anda tidak ingin membangun host runner dari nol, marketplace Cloudzy punya deployment sekali klik untuk GitLab yang di-host sendiri dan Gitea yang di-host sendiri yang membuat instance CI Anda berjalan dalam beberapa menit, di VPS yang bisa Anda ukur persis sesuai headroom headless Chromium yang dibutuhkan suite Anda.

Pertanyaan yang Sering Diajukan
Bagaimana cara menyiapkan visual regression testing di pipeline CI/CD?
Instal BackstopJS, arahkan backstop.json ke halaman target Anda, dan setel opsi laporan CI agar menghasilkan JUnit XML. Gunakan backstop reference --docker dan backstop test --docker saat menjalankannya dari mesin lokal supaya screenshot dirender di dalam image Docker yang ter-pin. Di CI, jalankan job di dalam image backstopjs/backstopjs dan panggil backstop test secara langsung, lalu publikasikan laporan JUnit dari backstop_data/ci_report/xunit.xml. Pakai runner self-hosted dengan Docker executor, dengan RAM minimal 4GB untuk headless Chromium.
Berapa RAM yang dibutuhkan headless Chromium di container CI?
Satu proses headless Chromium memakai sekitar 300–500MB saat idle dan melonjak ke 1–2GB saat menangkap screenshot halaman penuh. Untuk CI yang andal, rencanakan sekitar 4GB RAM per satu sampai dua job bersamaan, lalu tambahkan sekitar 2GB untuk setiap job paralel berikutnya. CPU sifatnya bursty dan bukan penghambat utama; RAM-lah yang menentukan berapa job yang bisa Anda jalankan sekaligus.
Kenapa tes visual regression saya lolos di lokal tapi gagal di CI?
Rendering browser berbeda antar lingkungan: font, anti-aliasing, mode headless versus headed, dan OS semuanya mengubah hasilnya. Solusinya adalah menjalankan pembuatan baseline dan pembandingannya di image Docker yang sama supaya keduanya dirender identik. Flag --docker milik BackstopJS melakukan ini dengan menjalankan capture di dalam image reference yang ter-pin.
Bagaimana cara memperbaiki error /dev/shm di Chromium pada Docker CI?
Nilai default /dev/shm di Docker adalah 64MB, terlalu kecil untuk renderer headless Chromium dan memicu crash tanpa pesan. Naikkan dengan shm_size: '2gb' di docker-compose.yml (atau --shm-size=2gb pada container job), atau berikan --disable-dev-shm-usage di flag peluncuran Chromium supaya dia menulis file shared memory ke /tmp.
Sebaiknya pakai Playwright toHaveScreenshot atau tool visual regression khusus?
toHaveScreenshot() bawaan Playwright sudah cukup untuk suite kecil: sekitar kurang dari lima puluh layar, tanpa library komponen bersama dan tanpa kebutuhan UI review PR. Pindah ke tool khusus seperti BackstopJS ketika Anda butuh alur persetujuan baseline yang layak, laporan review, dan rendering yang dinormalkan lewat Docker, yang jadi penting begitu jumlah baseline Anda bertambah.
Apakah Lost Pixel masih dipelihara?
Tidak. Lost Pixel diarsipkan pada April 2026 dan bukan kandidat untuk deployment baru. Gunakan BackstopJS untuk setup visual regression testing yang di-self-host.