· Tutorials · By Simon Chiu

Puppeteer Block Requests: Images, Fonts, Ads, Trackers

Block requests in Puppeteer with your own interception code: by resource type or host, with measured bytes and load time saved per type, and a CDP option.

To block requests in Puppeteer, turn on request interception and decide for each request whether to abort or continue it:

await page.setRequestInterception(true);
page.on('request', (req) => {
  if (req.resourceType() === 'image') req.abort();
  else req.continue();
});

That is the whole mechanism. The useful questions are what to block for a screenshot, what each block actually saves, and what it costs the picture. This post answers those with numbers from a test page, then gives you a complete blocker you can paste into a project. It belongs to our Puppeteer screenshot guide.

Tested with: Puppeteer 25.14.0, Chrome 155.0.8059.79, Node.js 22.18.0, macOS on an Apple M1 Pro. Tested on 2026-10-18.

What blocking saves, per resource type

We served a test page from a local Node.js server. It has a 28 KB stylesheet, two web fonts (about 750 KB each), twelve 53 KB PNG images, a 1.5 MB video with preload="auto", and two 90 KB third-party scripts loaded from a second origin. The server adds a fixed delay to each file type (images 120 ms, fonts 150 ms, scripts 400 ms) so the timing is repeatable.

For each rule we ran 5 fresh browser contexts and took the median. "Transferred" is the sum of encodedDataLength from the Chrome DevTools Protocol Network.loadingFinished events, measured at the load event. We ran the test twice: once on loopback (no bandwidth limit) and once throttled with Network.emulateNetworkConditions to 10 Mbit/s and 40 ms latency, which is closer to a real fetch over the internet.

Blocked Requests blocked Transferred (loopback) Load, loopback Load, 10 Mbit/s Screenshot PNG
Nothing 0 3,765 KB 457 ms 2,966 ms 51 KB
image 12 3,137 KB 439 ms 2,464 ms 11 KB
font 2 2,298 KB 436 ms 1,833 ms 51 KB
media 1 2,300 KB 440 ms 2,184 ms 51 KB
stylesheet 1 3,738 KB 454 ms 2,987 ms 51 KB
Third-party script 2 3,589 KB 418 ms 2,824 ms 51 KB
image + font + media 15 205 KB 441 ms 695 ms 11 KB

What we took from it:

  • On a fast network, blocking barely changes load time. Loopback load times stayed between 418 and 457 ms whatever we blocked, because the slowest file (a third-party script) sets the pace. Blocking pays off when bandwidth is the limit.
  • On a throttled network the big files dominate. Blocking fonts, images and video together cut load from about 3.0 s to 0.7 s. Transfer at load fell from 3.7 MB to 205 KB on loopback, and from 2.3 MB to 205 KB on the throttled link, where less of the video had arrived by then.
  • The video was the largest file but it did not hold load. Browsers do not wait for media data before load. Blocking it still saves bandwidth, and the saving shows in the throttled column because the video competes with everything else for the same link.
  • Blocking images changes the picture. The PNG dropped from 51 KB to 11 KB because the image boxes were empty. Fonts and stylesheets also change the picture: blocked fonts fall back to a system font, and a blocked stylesheet gives you unstyled HTML. Only ads, trackers, chat widgets and media are usually safe to drop for a screenshot.

The cost of interception itself

Request interception sends every request from Chrome to your Node.js process and waits for an answer. We measured that cost on the same page with 9 runs per mode:

Mode Median load (loopback)
No interception 420 ms
Interception on, every request continued 429 ms
Interception on, images, fonts and media aborted 426 ms
Network.setBlockedURLs (CDP), same files blocked 416 ms

On 20 requests the overhead is about 10 ms. It grows with the request count, and it grows faster when your Node.js event loop is busy with other pages, which is exactly the situation in a parallel screenshot service.

A complete blocker

This script blocks by resource type and by host, counts what it did, and exits with a non-zero code when the capture fails. The host list matches a domain and all its subdomains.

import puppeteer from 'puppeteer';

const BLOCKED_TYPES = new Set(['media', 'font']);
const BLOCKED_HOSTS = ['doubleclick.net', 'google-analytics.com', 'googletagmanager.com', 'hotjar.com', '127.0.0.1'];

function shouldBlock(req) {
  if (BLOCKED_TYPES.has(req.resourceType())) return true;
  const { hostname } = new URL(req.url());
  return BLOCKED_HOSTS.some((h) => hostname === h || hostname.endsWith(`.${h}`));
}

const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  const stats = { blocked: 0, allowed: 0 };

  await page.setRequestInterception(true);
  page.on('request', (req) => {
    if (req.isInterceptResolutionHandled()) return;
    if (shouldBlock(req)) {
      stats.blocked++;
      req.abort('blockedbyclient', 0);
    } else {
      stats.allowed++;
      req.continue({}, 0);
    }
  });

  await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
  await page.screenshot({ path: 'blocked.png' });
  console.log(stats);
} catch (err) {
  console.error(err);
  process.exitCode = 1;
} finally {
  await browser.close();
}

On our test page it printed { blocked: 5, allowed: 15 } (two fonts, one video, two scripts from the second origin, which is 127.0.0.1 in the list only for the test). On https://example.com it printed { blocked: 0, allowed: 2 }. Remove 127.0.0.1 from the list before you use it.

Three details in that handler matter:

  1. isInterceptResolutionHandled() returns early if another handler already resolved the request. Without it, two handlers that both call continue() throw "Request is already handled".
  2. The second argument to abort() and continue() is a priority. It turns on Puppeteer's cooperative intercept mode, where several handlers can vote and the highest priority wins. If you are the only handler, the plain calls work too.
  3. 'blockedbyclient' is the error code the page sees. It matches what an ad blocker produces, so page code that checks for blocked trackers behaves as it would for a real visitor.

Block by URL pattern instead of host

When a vendor serves trackers from its main domain, match on the path:

const BLOCKED_PATHS = [/\/collect\b/, /\/pixel\b/, /\/beacon\b/];
const isTrackingPath = (req) => BLOCKED_PATHS.some((re) => re.test(new URL(req.url()).pathname));

Answer instead of abort

Some pages break when a script fails to load: an onerror handler shows a warning, or a script waits for a global the blocked file would define. Answer with an empty response instead, so the page sees success:

page.on('request', (req) => {
  if (req.isInterceptResolutionHandled()) return;
  if (shouldBlock(req) && req.resourceType() === 'script') {
    req.respond({ status: 200, contentType: 'application/javascript', body: '' }, 0);
  } else if (shouldBlock(req)) {
    req.abort('blockedbyclient', 0);
  } else {
    req.continue({}, 0);
  }
});

Without interception: Network.setBlockedURLs

Chrome can block URLs itself through the DevTools Protocol. No request makes a round trip to Node.js, and you do not have to resolve every request. The method is marked experimental in the protocol, so pin your Puppeteer version and re-test on upgrades.

import puppeteer from 'puppeteer';

const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  const cdp = await page.createCDPSession();
  await cdp.send('Network.enable');
  // Chrome matches these patterns itself; no request round-trips to Node.
  await cdp.send('Network.setBlockedURLs', {
    urls: ['*doubleclick.net*', '*google-analytics.com*', '*.mp4', '*.woff2', '*.ttf'],
  });
  let failed = 0;
  page.on('requestfailed', () => failed++);
  await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
  await page.screenshot({ path: 'blocked-cdp.png' });
  console.log({ blocked: failed });
} catch (err) {
  console.error(err);
  process.exitCode = 1;
} finally {
  await browser.close();
}

The trade-off: patterns are simple wildcards on the URL, so you cannot block by resource type, and you cannot respond with an empty body. Use it for a fixed list of hosts and file extensions; use interception when the decision needs logic.

What to block for screenshots

Category Block? Why
Ad networks Yes Ads change on every load, push content down, and are a common source of slow third-party requests
Analytics and tracking Yes Invisible in the picture; they also keep connections open, which can delay network idle waits
Chat widgets Yes They cover content and often open long-lived connections
Video and audio Usually Large, and a paused video still shows its poster when the poster is an image
Images Only for text captures Blocking them empties the image boxes in the screenshot
Web fonts Rarely The text renders in a fallback font and the layout can shift
Stylesheets No The page renders unstyled
First-party scripts No Client-rendered pages render nothing without them

Cookie banners are a different problem: blocking the consent script often leaves an empty overlay or a page that refuses to scroll. Our post on blocking cookie banners at scale covers that case. For Puppeteer code that removes them, see hiding cookie banners and chat widgets in Puppeteer.

Troubleshooting

Symptom Cause Fix
Page never finishes loading A request reached the handler and was never resolved Make every branch call abort(), continue() or respond()
Error: Request is already handled! Two handlers resolved the same request Check req.isInterceptResolutionHandled() first, and pass a priority
A rule never matches You compared req.url() as a string and the URL has a port, query or uppercase host Parse with new URL(req.url()) and compare hostname
Two handlers fight over requests A library you use also intercepts Use cooperative mode: pass a priority to every abort(), continue() and respond() call
Page shows "please disable your ad blocker" The page detects failed tracker scripts Respond with an empty script instead of aborting
Layout broken after blocking You blocked a stylesheet, font or first-party script Narrow the rule to third-party hosts
Fewer bytes saved than expected Service worker served the files from its cache Call page.setBypassServiceWorker(true) before goto()
Blocking had no effect on load time Network is fast and one slow file sets the pace Expected; the savings show on slow or metered networks

The same blocking with one API call

RenderScreenshot has the common categories built in, plus your own patterns and resource types (see blocking options). Ads, trackers, chat widgets and cookie banners use lists we maintain, and blockUrls() takes glob patterns:

import { writeFile } from 'node:fs/promises';
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);

const image = await client.take(
  TakeOptions.url('https://example.com')
    .blockAds()
    .blockTrackers()
    .blockChatWidgets()
    .blockUrls(['*hotjar*', '*/beacon/*'])
    .blockResources(['media'])
);
await writeFile('clean.png', image);

The ads and cookie banner guide shows which option fits which kind of site. Create a free account to get 150 credits every month and try it on your own URLs.

Related in this topic