ScreenshotNeo

BlogHow-to

How to Compare Website Screenshots on Low Bandwidth Using Playwright

Use Playwright screenshot assertions for visual diffs, and separate offline, mocked-request, and genuinely bandwidth-shaped tests so results mean what they say.

By the ScreenshotNeo team4 October 202610 min read

Use Playwright Test’s toHaveScreenshot() assertion to compare a page or locator against a stored screenshot. Low bandwidth is a separate test condition: Playwright routing can mock, replay, or abort requests, and its offline option simulates no connectivity. Those controls do not impose a measured slow-connection profile. If you need a specific throughput or latency, shape the network in a separately verified test environment and document its settings.

Keep these three questions separate: did the pixels change, which requests were available, and what network conditions did the browser actually experience? A screenshot diff answers only the first.

1. Set up a screenshot comparison

Screenshot assertions are part of Playwright Test. The first run creates a reference image; later runs compare the current capture with that reference. Keep the browser, operating system, and rendering conditions consistent between baseline creation and comparison because differences in the environment can affect rendered pixels.

Install and configure

npm init playwright@latest

In the generated project, create or edit a test such as tests/visual.spec.ts:

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

test('homepage matches its visual reference', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('homepage.png');
});

Run the test with the project’s configured Playwright Test runner:

npx playwright test tests/visual.spec.ts

On its first run, the runner writes the reference screenshot. Review and keep that image with the test. On later runs, the assertion compares the new capture against it and reports visual differences. Use .png for the documented default format; a .webp filename requests WebP output.

Capture a whole page or one element

A page assertion captures the page; a locator assertion narrows the comparison to a component. For example:

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

test('checkout summary matches its reference', async ({ page }) => {
  await page.goto('https://example.com/checkout');
  const summary = page.locator('[data-testid="checkout-summary"]');
  await expect(summary).toHaveScreenshot('checkout-summary.png');
});

Use a locator when unrelated page regions are dynamic or when the feature under test is a specific component. Make sure the selector identifies the intended element uniquely and that the element is visible and ready before asserting.

2. Decide what “low bandwidth” means for this test

Scenario Playwright control What it establishes What it does not establish
Visual regression toHaveScreenshot() Whether the rendered capture differs from a stored reference Any particular network speed
Deterministic or degraded resources Routing, request fulfillment, HAR replay, or aborts How the page behaves with selected responses or missing resources A realistic throughput or latency profile
No connectivity offline context option Behavior when the browser has no network connectivity A slow connection
Measured slow connection A separately verified network-shaping mechanism in the test environment Only the throughput, latency, and any other conditions that mechanism actually applies A profile unless its settings and scope are recorded and verified

The reviewed Playwright documentation describes offline emulation and request control; it does not establish a built-in bandwidth-throttling API. Do not label mocked or aborted requests as throttling. If you use external shaping, verify whether it applies to the browser process, host, or entire test environment, and record configured throughput, latency, and packet loss where applicable. (The Playwright docs do not prescribe a particular shaping tool or command.)

3. Control requests for repeatable degraded-resource tests

Install routes before navigation so the relevant requests are intercepted from the start. Fulfill predictable API responses to stabilize page data. Replay a HAR when you want recorded responses. Abort selected requests to check how the interface handles resources that fail to load.

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

test('page remains usable when an image request fails', async ({ page }) => {
  await page.route('**/*.{png,jpg,jpeg,webp}', route => route.abort());
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('homepage-with-images-unavailable.png');
});

This test deliberately makes matching image requests fail. It does not slow other requests or represent a measured low-bandwidth connection. Choose a narrow route pattern so the test does not accidentally block scripts, stylesheets, or unrelated assets.

Fulfill a predictable API response

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

test('dashboard matches with a fixed API response', async ({ page }) => {
  await page.route('**/api/dashboard', route => route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ title: 'Usage', total: 42 }),
  }));

  await page.goto('https://example.com/dashboard');
  await expect(page).toHaveScreenshot('dashboard-fixed-data.png');
});

Adjust the route pattern and response body to match the application’s actual endpoint and schema. A deterministic response makes the data condition repeatable; it does not reproduce the timing of a real slow server.

Replay recorded network traffic

Playwright supports recording and replaying HAR files through routing. A HAR can provide repeatable responses for recorded requests. Follow the Playwright network documentation for the version you use, including its guidance about service workers and routing. A replayed response is still controlled traffic, not evidence of limited bandwidth.

4. Test a fully offline page separately

Set the context’s documented offline option for a no-network scenario. In a Playwright Test project, configure it for the relevant test or project:

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

test.use({ offline: true });

test('offline state is rendered', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('homepage-offline.png');
});

The exact page behavior depends on the app and whether it has cached or service-worker-provided content. Treat this as an offline test, and assert the intended offline UI where possible. It is not a slow-network test: no network connectivity and constrained network connectivity are different conditions.

5. Keep visual results stable

Visual comparisons can change with operating system, browser version, browser settings, hardware, power source, and headless mode. Use the same environment for baseline generation and comparison whenever possible. Make the baseline from the same browser project and operating system used in continuous integration, and update it deliberately when the intended design changes.

  • Wait for the real page state. Navigate to the relevant route and wait for a meaningful application condition before capturing; do not rely on an arbitrary short delay if a locator or app signal is available.
  • Control dynamic content. Use stable fixtures or deterministic route responses. Playwright supports a screenshot stylesheet for hiding volatile elements during capture; use it only for content that is irrelevant to the comparison.
  • Choose the right scope. A full-page assertion can catch broad layout changes; a locator assertion reduces unrelated visual noise for a component.
  • Set diff tolerance deliberately. Screenshot assertion options allow control over image-diff thresholds. A permissive threshold can conceal meaningful changes; a strict comparison can flag harmless rendering variation.
  • Keep network conditions in the test record. Name the scenario precisely—such as “API response mocked,” “images aborted,” “offline,” or “shaped to configured profile”—and include shaping settings when applicable.

Screenshot assertions wait for two consecutive screenshots to match before comparing, helping avoid capturing an intermediate rendering state. This stabilization does not make an uncontrolled external service or a changing fixture deterministic.

6. Read and interpret the diff

When an assertion fails, inspect the actual screenshot, expected reference, and reported difference image. Determine whether the mismatch is an intended UI change, an uncontrolled input, an environment change, or a failure condition the test is meant to expose. Update the reference only after confirming the new rendering is expected; otherwise fix the source of instability or the page behavior.

For a low-bandwidth investigation, also verify that the intended network mechanism really applied to the browser under test. The image diff reports pixels; it does not certify a network profile or prove the page experienced a specific connection speed.

7. Troubleshooting

Symptom Likely cause Fix
The first screenshot assertion fails because no reference exists This is the initial baseline run Review the generated reference, then keep it with the test so later runs have an expected image.
Images or fonts are missing in the capture Requests were aborted, routes were registered too late, or a resource failed Check route patterns and install routes before navigation. If testing missing resources, assert that scenario explicitly.
The test is described as “slow network,” but the page only shows missing assets Aborting requests was mistaken for bandwidth throttling Rename the case as a failed-resource test, or use a separately verified shaping mechanism and report its conditions.
The test reports offline behavior when a slow connection was intended offline: true removes connectivity rather than limiting speed Use offline only for no-connectivity behavior. Apply and verify separate network shaping for a slow connection.
Reference images differ across local and CI runs Browser, operating system, settings, hardware, power, or headless mode differ Align the baseline and comparison environment, then regenerate the reference only when the change is intended.
A visual assertion catches transient content Data, animation, timestamps, or other page state changes between captures Use stable fixtures or routes, wait for an application-specific ready condition, and hide only irrelevant volatile elements with the screenshot stylesheet.
A route does not appear to intercept a request The route pattern does not match, routing was registered after navigation, or service-worker behavior affects interception Check the URL pattern, register before navigation, and consult Playwright’s network guidance on service workers.
Small rendering differences cause repeated failures The comparison environment varies or the diff threshold is too strict for the intended test Stabilize the environment and dynamic inputs first; adjust the threshold only with a clear reason.

8. Performance, reliability, and cost

Visual screenshot tests add browser rendering and image comparison work to a test run. Keep suites efficient by capturing the scope that answers the question: a locator for a component, a page capture for a page-level regression. Reuse stable test data and avoid repeating expensive setup without need. The sources cited here provide no benchmark for capture speed or suite cost, so measure those in your own runner.

Reliability depends on stable rendering inputs and matching browser environments. A deterministic mock can improve repeatability while reducing realism; a HAR replay provides recorded responses but still does not model constrained throughput. A shaped test can address slow-network behavior only when the shaping mechanism is configured, scoped, and verified. Report these limits alongside results so another developer can reproduce the scenario.

Playwright is an open-source browser automation framework; infrastructure and CI costs depend on where and how tests run. There is no per-screenshot Playwright API price stated in the sources used for this guide.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. Its clean-capture flow accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. AI agents can use its MCP tools for screenshots, page information, and PDF capture.

To compare the returned image with a locally stored reference, save the API response as an image and use it as input to your image-diff workflow. This call captures a page; it does not configure low-bandwidth shaping or replace Playwright’s browser assertions.

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

See the ScreenshotNeo API documentation for options and parameter details. The same endpoint can be called from Python or Node.js:

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);

The Node.js example uses Bun.write to save the response. In Node.js without Bun, write the response bytes with your preferred filesystem method.

ScreenshotNeo has full-page capture with lazy images loaded, element capture, dark mode, device presets and custom viewports, retina scale, PDF settings, HTML/CSS-to-image, custom CSS and JavaScript, click-before-capture, selector hiding, wait conditions, request and resource blocking, custom headers and cookies, user agent and authorization, timezone and geolocation, transparent backgrounds, resizing, configurable cache TTL, signed public image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI spec. Common parameter names used by other screenshot APIs also work. Those capture controls do not establish a controlled network-throttling profile.

Plans are Free: 1,000 shots a month with no card; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Start with 1,000 free screenshots a month; no card required.

10. FAQ

Can Playwright compare screenshots captured on two different runs?

Yes. Playwright Test’s screenshot assertion compares the current capture with the stored reference created by an earlier run. Keep rendering conditions aligned so the diff reflects page changes.

Does an image diff tell me whether my bandwidth profile worked?

No. It tells you whether rendered pixels differ. Verify and report the network-shaping setup independently.

Should I use a page or locator screenshot?

Use a page capture for page-level visual changes and a locator capture when one component is the subject or surrounding content is unrelated.

Is HAR replay a substitute for throttling?

No. HAR replay supplies recorded responses for repeatable request behavior; it does not establish limited throughput or latency.

Primary references