What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a DOM/CSS override when you only need a different appearance; intercept image requests when the page must receive different image bytes. In Playwright, inject styles or swap sources immediately before the screenshot. In Puppeteer, enable request interception and respond with fixture files for image requests. Whichever layer you choose, wait for decoding and layout to settle, disable motion, and keep the rendering environment fixed so replacements produce repeatable pixels.

Choose the replacement layer first

Situation Recommended method Why
Existing <img> or CSS background; the page layout should stay the same DOM/CSS override Fast and local, while preserving the element’s box when sizing is matched.
The page must consume different image bytes, or images are created dynamically Network interception Substitutes the resource before rendering, including elements inserted after navigation.
Images come from a third-party host or expiring URLs URL or resource-type interception Keeps tests independent of unstable remote assets.
Visual-regression baselines Either method plus motion and environment controls Reduces pixels that differ for reasons unrelated to your product.

Playwright: replace an image for one screenshot

Playwright can apply a stylesheet only for the screenshot. This is ideal when you want to hide an image, paint a fixture behind it, or alter a CSS background without changing application state. Registering the style in page.screenshot keeps the override scoped to that capture.

Screenshot-only CSS override

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'networkidle' });

await page.screenshot({
  path: 'page.png',
  style: `
    img.hero {
      content-visibility: hidden;
      background: url('file:///tmp/replacement.png') center/cover no-repeat;
    }
    .profile-card {
      background-image: url('file:///tmp/replacement.png') !important;
      background-size: cover;
      background-position: center;
    }
  `,
  animations: 'disabled'
});

await browser.close();

Use a selector that is as narrow as possible. A broad img rule can unintentionally alter logos, icons, or tracking pixels. The style is applied through Shadow DOM and inner frames by Playwright’s screenshot styling support, so it can cover components that ordinary page-level CSS cannot reach.

Swap the source and wait for decoding

If application code must see the replacement URL, set the image source in the page, then wait for the new resource to finish loading and decode. Waiting only for navigation is insufficient when the swap happens afterward.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.goto('https://example.com', { waitUntil: 'networkidle' });

await page.evaluate(() => {
  const image = document.querySelector('img.hero');
  if (!image) throw new Error('img.hero was not found');
  image.src = '/fixtures/hero-replacement.png';
  image.style.objectFit = 'cover';
});

await page.waitForFunction(() => {
  const image = document.querySelector('img.hero');
  return image && image.complete && image.naturalWidth > 0;
});
await page.evaluate(async () => {
  const image = document.querySelector('img.hero');
  if (image && image.decode) await image.decode();
});
await page.screenshot({ path: 'page.png', animations: 'disabled' });

For a CSS background, set element.style.backgroundImage and wait for an explicit preload promise. Keep the replacement’s aspect ratio and object-fit rules aligned with the original if layout must not move.

Playwright: replace response bytes with routing

Routing is the better fit when the browser should receive a real fixture response, when images are lazy-loaded, or when the DOM is generated after navigation. Register the route before goto; otherwise early requests can escape the handler.

import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext({ serviceWorkers: 'block' });
const page = await context.newPage();

await page.route('**/*', async route => {
  const request = route.request();
  if (request.resourceType() === 'image') {
    await route.fulfill({
      path: './fixtures/replacement.png',
      contentType: 'image/png'
    });
  } else {
    await route.continue();
  }
});

await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'page.png', fullPage: true, animations: 'disabled' });
await browser.close();

A URL pattern can be safer than replacing every image:

Rank #2
Free Fling File Transfer Software for Windows [PC Download]
  • Intuitive interface of a conventional FTP client
  • Easy and Reliable FTP Site Maintenance.
  • FTP Automation and Synchronization
await page.route('**/avatars/**', async route => {
  await route.fulfill({ path: './fixtures/avatar.png', contentType: 'image/png' });
});

Blocking service workers is important when a service worker owns the request; otherwise the page API may not observe the network transaction you expected to replace. If the site uses data URLs or inline SVG, there is no image request to intercept, so use a DOM/CSS override instead.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Puppeteer: intercept and respond to image requests

Puppeteer’s interception lifecycle is explicit. After interception is enabled, every request stalls until it is continued, aborted, or fulfilled (or completed from the browser cache). Forgetting to resolve even one request can make navigation hang.

const puppeteer = require('puppeteer');
const fs = require('fs');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const replacementPngBuffer = fs.readFileSync('./fixtures/replacement.png');

  await page.setRequestInterception(true);
  page.on('request', request => {
    if (request.resourceType() === 'image') {
      request.respond({
        status: 200,
        contentType: 'image/png',
        body: replacementPngBuffer
      });
    } else {
      request.continue();
    }
  });

  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  await page.screenshot({ path: 'page.png', fullPage: true });
  await browser.close();
})();

Abort, pass through, or target by URL

  • Call request.respond() with a status, MIME type, and bytes to provide a fixture.
  • Call request.abort() to suppress an image and test the broken or empty state.
  • Call request.continue() for every request you do not replace.
  • Check request.url() as well as resourceType() when only one CDN path should change.

Set the correct contentType. Returning PNG bytes as JPEG can produce decode failures or a broken-image icon. If a page requests WebP or AVIF and your fixture is PNG, responding with the fixture’s actual MIME type is safer than copying the original header.

Make the screenshot deterministic

Wait for the replacement, not just navigation

Capture only after the replacement reports a successful load and, where available, decode() resolves. Also wait for the UI condition that triggers lazy loading. A full-page capture can reveal images below the initial viewport that were never requested yet.

Freeze motion and time-dependent UI

Disable CSS animations and transitions for regression captures. Playwright’s screenshot assertions disable animations and wait for two consecutive screenshots to be identical before comparison; use equivalent settling logic in custom Puppeteer code. Remove blinking carets, rotating carousels, and hover effects with a test stylesheet or deterministic input state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep rendering inputs fixed

  • Pin the browser version and operating system image.
  • Use the same viewport, device scale factor, fonts, color scheme, timezone, and locale.
  • Choose headless or headed mode consistently and avoid changing hardware or power conditions between baseline and comparison runs.
  • Use CSS-pixel scaling for stable dimensions; use a device scale factor when the test specifically needs high-DPI output.

Browser rendering can vary with the host OS, browser version, settings, hardware, power source, headless mode, and related environment details. Treat those as part of the baseline, not incidental infrastructure.

Common failures and fixes

Symptom Likely cause Fix
Old image still appears Override or route was installed after the request, or a service worker supplied the response. Register before navigation; block service workers for interception; clear or isolate the browser context.
Broken-image icon Capture happened before load/decode, or response bytes and MIME type disagree. Wait for complete, naturalWidth, and decode(); return the correct content type.
Navigation hangs in Puppeteer A request was intercepted but never resolved. Ensure every branch calls respond, abort, or continue.
Only images above the fold changed Lazy loading deferred lower-page requests. Trigger scrolling or use fullPage after the page has loaded the required selectors.
Layout shifts after replacement Fixture dimensions or object-fit differ from the original. Match intrinsic dimensions, aspect ratio, CSS sizing, and reserved space.
Some “images” never intercept They are CSS backgrounds, inline SVG, canvas drawings, or data URLs. Use DOM/CSS replacement or modify the drawing/input data instead of relying on network interception.
Cross-origin fixture cannot load The browser blocks a file URL or the page’s policy rejects the source. Serve fixtures from an HTTP origin allowed by the test, or fulfill the network request directly.

Performance, reliability, and maintenance

  • Scope routes narrowly. Matching one host or path avoids processing fonts, scripts, and analytics unnecessarily.
  • Reuse browser contexts carefully. A fresh context prevents cookies, cache, and service-worker state from leaking between tests; reuse one only when isolation is not required.
  • Keep fixtures versioned. Store replacement files with the test so a CDN change cannot silently alter a baseline.
  • Prefer local bytes for repeatability. Remote fixtures add DNS, TLS, CDN, and availability variables.
  • Record why a replacement occurred. Log the matched URL and fixture name, but do not log secrets in custom headers or signed URLs.
  • Validate the failure path. Intentionally abort one image and verify that your application shows its designed fallback instead of masking a production bug.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server for developers. One GET request returns PNG, JPEG, WebP, or PDF, while its capture pipeline accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed.

For a one-off replacement workflow, host the desired image in the page or use ScreenshotNeo’s custom CSS and JavaScript options to alter the target before capture. The API also supports element capture, full-page lazy-image loading, device presets, arbitrary viewports, retina scale, dark mode, headers, cookies, user agents, authorization, timezone, geolocation, request blocking, waits, click actions, hiding selectors, resizing, transparent backgrounds, selectable cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification.

cURL:

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

Python:

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)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for request options. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients, so an AI agent can run captures without you maintaining browser interception code. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

FAQ

Should I replace images in the DOM or at the network layer?

Use DOM/CSS when appearance is the only concern. Use interception when application code, lazy loading, or dynamic insertion must receive replacement bytes.

Can I replace a CSS background with request interception?

Only if the browser requests the background as a network resource. Inline styles, data URLs, and inline SVG require a CSS or DOM change.

Why does a screenshot differ on another machine?

Rendering depends on the host OS, browser build, fonts, hardware, power state, headless mode, and related settings. Pin those inputs for visual tests.