ScreenshotNeo

BlogHow-to

How to Fix Inconsistent Website Screenshots Across Runs

Find why website screenshots change between runs and stabilize visual tests by pinning the browser, controlling data, and waiting for the right page state.

By the ScreenshotNeo team4 October 20269 min read

Website screenshots differ across runs when the page state or rendering environment is not deterministic. Pin the browser and operating system, fix the viewport and device scale, control API responses and test data, wait for a meaningful ready condition and required assets, and handle animation or volatile regions deliberately. Then inspect the diff and run context before updating a baseline.

This guide shows a runnable Playwright setup, a repeatable debugging workflow, common fixes, and how to decide whether a difference is test noise or a real UI change. It also applies to flaky visual regression tests in other browser tools.

1. Identify what can change between captures

A screenshot records pixels produced by a particular page state in a particular rendering environment. A stable application can still look different if the host OS, browser version, browser settings, hardware, power source, or headless mode changed. Playwright recommends generating and comparing screenshots in the same environment; its best practices also advise keeping OS and browser versions the same for visual checks. Playwright visual comparisons · Playwright best practices

Source of variation What to check Typical correction
Rendering environment OS or container image, browser version, headed/headless mode, viewport, device scale factor, fonts Use the same pinned CI image and browser project for baseline and comparison runs.
Changing page data API responses, timestamps, random values, rotating content, third-party widgets Use fixtures or stub responses; freeze values that the test does not intend to cover.
Capture timing Loading indicators, fonts, CSS, images, lazy content, delayed components Wait for the application’s ready condition and required assets, not an arbitrary long sleep.
Motion and volatile regions CSS animation, video, carousels, clocks, cursors, live counters Disable or freeze motion for the visual assertion; mask only intentionally irrelevant regions.
Network and cache Failed, slow, or uncontrolled requests; stale service worker responses Inspect request failures, control required responses, and make cache behavior intentional.

2. Make the capture environment repeatable

  1. Use the same browser engine and exact browser revision when generating and checking the baseline.
  2. Run screenshots in the same OS/container image. Avoid comparing a developer’s local rendering with a different CI renderer unless separate baselines are intended.
  3. Set viewport dimensions and device scale factor explicitly. Keep browser settings such as color scheme consistent too.
  4. Record the browser, OS/image, viewport, device scale, and relevant settings with the run. When a failure appears, compare this metadata before changing the baseline.

Playwright names visual snapshots with browser and platform details by default, and notes that rendering differences across browsers and platforms can require separate snapshots. If your product supports multiple browsers or devices, create and review a baseline for each supported environment rather than treating their pixels as interchangeable. Playwright snapshot naming and visual comparisons

3. Control data and network responses

Make the same page render the same content on each run. Use fixed fixtures for dates, user profiles, product lists, and other variable data. Stub API calls that are not the subject of the visual test. Playwright recommends using its Network API to guarantee the response a test needs; Cypress recommends fixtures and network stubbing for repeatable visual testing. Playwright best practices · Cypress visual testing

Register routes before navigation so the page cannot race ahead and issue an uncontrolled request. Match only the endpoints needed by the test; letting unrelated requests proceed can preserve useful behavior, while blocking every request may leave fonts, styles, or images missing.

4. Wait for the page state and assets you need

Choose a readiness signal that corresponds to the visual state under test: for example, a loaded-results marker, a component’s final state, or a specific heading. Then verify that its fonts, CSS, images, and other required assets have loaded. Network idle can help for pages whose traffic settles, but analytics, polling, or long-lived connections can prevent it from being a useful universal condition.

A blind sleep is not a general fix. It adds time and can still capture too early when a slow asset or lazy-loaded section arrives later. Percy troubleshooting specifically calls out network timeouts, missing fonts or CSS, and lazy-loaded assets; its lazy-loading guidance explains that scrolling can be needed to trigger images that load on intersection. Percy troubleshooting · Percy lazy-loading guidance

5. Runnable Playwright example

This example pins a viewport, stubs a changing API response, waits for a page-specific ready marker and fonts, and takes a screenshot assertion. It is intended to run in a consistent Playwright environment with its browser installed.

npm init -y
npm install --save-dev @playwright/test
npx playwright install chromium

Create playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  use: {
    browserName: 'chromium',
    headless: true,
    viewport: { width: 1280, height: 800 },
    deviceScaleFactor: 1,
    colorScheme: 'light',
    reducedMotion: 'reduce',
  },
  expect: {
    toHaveScreenshot: {
      animations: 'disabled',
      caret: 'hide',
    },
  },
});

Create tests/home.spec.ts, replacing the example origin and selectors with your application’s:

import { test, expect } from '@playwright/test';

test('home page has a stable visual state', async ({ page }) => {
  // Install the deterministic response before the page can request it.
  await page.route('**/api/products', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 'one', name: 'Sample item', price: 12 },
        { id: 'two', name: 'Second item', price: 24 },
      ]),
    });
  });

  await page.goto('http://127.0.0.1:3000/', { waitUntil: 'domcontentloaded' });
  await page.getByTestId('home-ready').waitFor({ state: 'visible' });
  await page.evaluate(() => document.fonts.ready);

  // For pages with lazy content, scroll it into view before the assertion.
  await page.getByTestId('product-list').scrollIntoViewIfNeeded();
  await expect(page).toHaveScreenshot('home.png', { fullPage: true });
});

Start the app separately, then run npx playwright test. The first run creates a reference image; review and commit it only after confirming it represents the intended UI. Later runs compare against that file. Playwright’s screenshot assertion captures until two consecutive images match, which can reduce transient capture noise, but it does not make changing data, environments, or missing assets deterministic. Playwright screenshot assertions

Useful Playwright screenshot controls

  • fullPage: true captures the full scrollable page; ensure lazy content is actually requested before asserting.
  • animations: 'disabled' disables finite animations and fast-forwards infinite animations for the screenshot. Keep a separate test if animation itself is the behavior being tested.
  • caret: 'hide' prevents a blinking text cursor from creating a pixel difference.
  • stylePath can apply a screenshot-only stylesheet to hide known volatile elements. Use it narrowly so meaningful layout changes remain visible.
  • maxDiffPixels or maxDiffPixelRatio can allow a defined amount of pixel difference. These tolerances do not fix flakiness; set them only after understanding the difference.
  • Locator screenshots can narrow the assertion to a component when the page shell contains unrelated volatile content.

See the current option list and semantics in Playwright’s visual comparison documentation.

6. Adapt the same idea to Cypress

In Cypress, load fixtures and intercept the changing endpoint before visiting the page, then wait for the aliased response and an application-level ready marker before using your screenshot assertion tool. For example, the setup pattern is:

cy.intercept('GET', '/api/products', { fixture: 'products.json' }).as('products');
cy.visit('/');
cy.wait('@products');
cy.get('[data-testid="home-ready"]').should('be.visible');
cy.get('[data-testid="product-list"]').screenshot();

The final .screenshot() command depends on the screenshot or visual testing plugin installed in your project; Cypress core’s screenshot API and third-party visual comparison integrations have different baseline workflows. Cypress documents that waitForAnimations and animationDistanceThreshold apply to action commands. Do not assume those action settings alone stabilize screenshot capture; use your capture tool’s documented controls or test-only CSS. Cypress visual testing

7. Diagnose a diff before updating the baseline

  1. Compare run context. Check browser version, OS/container, viewport, device scale, color scheme, and headless mode.
  2. Compare page data. Confirm fixtures and intercepted responses are identical. Look for timestamps, random IDs, rotation, and external content.
  3. Inspect network failures. Check status codes and timing for fonts, stylesheets, images, API calls, and protected assets.
  4. Check page readiness. Confirm the app reached its final state and lazy-loaded sections were triggered before capture.
  5. Review motion and volatile content. Freeze animations, clocks, or carousels only if those behaviors are outside the test’s purpose.
  6. Reproduce in the pinned environment. If the same diff recurs with deterministic data and complete assets, treat it as a likely UI change and investigate.
  7. Update the baseline only after review. A baseline update can hide a real regression; inspect the image diff and confirm the expected design change first.

For managed comparison workflows, compare how tightly the browser and OS are controlled, how assets and dynamic regions are handled, how the tool integrates with CI, and how reviewers accept or reject changes. Percy’s troubleshooting docs recommend inspecting network and asset issues when snapshots are incomplete. Percy troubleshooting guide

8. Troubleshooting common failures

Symptom Likely cause Fix
Text differs slightly across CI and local Different OS, browser build, or installed fonts Generate and compare baselines in the same pinned image and browser version; ensure required fonts are available.
Screenshot shows a spinner or skeleton Capture ran before the application reached its ready state Wait for a specific loaded-state signal and inspect slow or failed requests; do not rely on a larger fixed sleep alone.
Images or styles are missing Request failed, host is inaccessible, authorization is missing, or asset discovery timed out Inspect network logs, fix reachability and authentication, and adjust a documented timeout only when the asset genuinely needs longer.
Images below the fold are blank Lazy loading has not been triggered Scroll the relevant page sections or elements into view before capture, then wait for the image to load.
Diff moves around between runs Animation, caret, timestamp, carousel, or rotating data Disable animation for capture, hide the caret, and freeze or narrowly mask intentionally volatile regions.
Network-idle wait never finishes Polling, analytics, or a persistent connection keeps the page active Wait for a specific UI condition and required assets instead; stub irrelevant traffic if it affects the tested page.
Snapshot changes after dependency update Browser or rendering dependency changed Review the browser and OS change, regenerate baselines intentionally, and keep the new environment pinned.
A larger pixel tolerance makes the failure pass The assertion tolerates a difference without removing its cause First classify the diff. Keep a tolerance only when small rendering variation is acceptable and meaningful regressions still fail.

Percy’s docs specifically identify missing assets, network timeouts, fonts/CSS, lazy loading, and page-load failures as troubleshooting areas. Percy common issues

9. Performance, reliability, and cost

Visual assertions add browser work, and full-page captures can take longer than checking one stable component. Keep screenshot scope focused, avoid redundant captures, and stub slow external dependencies. A readiness condition makes runs more reliable than adding a large sleep to every test, while the fixed environment reduces unexplained baseline churn.

Do not trade reliability for speed by skipping asset checks or accepting broad pixel differences. If screenshots run in CI, a pinned container and browser install make the rendering environment reproducible; if a managed service is considered, include environment control, asset handling, dynamic-region controls, CI integration, and review workflow in the evaluation. This dossier provides no verified pricing or benchmark comparison for visual testing services.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. For a one-call capture, see the API documentation:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Cookie banners, newsletter popups, and chat widgets are removed before the shot, and each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed; response headers report the page verdict and billing status. An MCP server gives AI agents screenshot, page-info, and PDF capture tools. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots.

Sign up for 1,000 free screenshots a month, no card required.

FAQ

Why do screenshots match locally but fail in CI?

The local and CI rendering environments may differ. Compare OS image, browser version, headless mode, viewport, device scale, and fonts first.

Does Playwright automatically make visual tests deterministic?

No. Its screenshot assertion waits until consecutive captures match, which can help with short-lived visual instability. You still need stable data, environment, assets, and page readiness.

Should I hide every dynamic element?

No. Freeze or mask only content irrelevant to the assertion. Hiding a meaningful section can conceal a real layout or content regression.

Should I update the snapshot whenever a test fails?

No. First establish whether the difference is an intended UI change. Review the diff and run context before accepting a new baseline.