Speed Up Puppeteer Scraping by Blocking Images, Fonts, and Trackers
Use Puppeteer's request interception to abort images, fonts, and trackers, cutting scrape time and bandwidth — with a worked example, mocking via request.respond(), and the failure modes to watch for.
26 Jul 2026, 10:53 UTC

The short version
If your Puppeteer job only needs DOM text or structured data, most of what the browser downloads — images, webfonts, stylesheets, analytics scripts — is wasted bandwidth and wasted time. Enabling request interception with page.setRequestInterception(true) and aborting those resource types typically cuts page-load time substantially, and it costs you about ten lines of code. The trade-off: interception adds per-request overhead, disables some HTTP cache behavior, and can break pages whose layout or login flow depends on the resources you block. Measure before and after, and block selectively.
How interception works
Puppeteer talks to Chrome over the DevTools Protocol. When you enable request interception, Chrome pauses every outgoing network request and hands it to your callback. That callback must resolve every request by calling exactly one of:
request.continue()— let the request proceed (optionally modified).request.abort()— cancel it; the page sees a failed request.request.respond()— answer it yourself with a synthetic status, headers, and body.
Forgetting to call one of these on any code path hangs the page load indefinitely. This is the single most common bug in interception code — an early return or a thrown exception that skips the resolution. Only one interception handler is active per page, so all your routing logic lives in one callback.
A worked example
This script blocks images, fonts, media, and stylesheets, plus any request to a small denylist of tracking domains, and logs what it did so you can verify the savings:
import puppeteer from 'puppeteer';
const BLOCKED_TYPES = new Set(['image', 'font', 'media', 'stylesheet']);
const BLOCKED_HOSTS = ['googletagmanager.com', 'google-analytics.com', 'doubleclick.net'];
const stats = { continued: 0, aborted: 0, byType: {} };
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', (req) => {
const type = req.resourceType();
const url = req.url();
const shouldBlock =
BLOCKED_TYPES.has(type) ||
BLOCKED_HOSTS.some((host) => url.includes(host));
if (shouldBlock) {
stats.aborted++;
stats.byType[type] = (stats.byType[type] ?? 0) + 1;
req.abort();
} else {
stats.continued++;
req.continue();
}
});
const start = Date.now();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(`Loaded in ${Date.now() - start} ms`);
console.log(stats);
const text = await page.evaluate(() => document.body.innerText);
await browser.close();Run it with Node 18+ (top-level await in an ES module) after npm install puppeteer. No special permissions are needed; Puppeteer downloads its own Chromium build. To measure the benefit, run the same navigation twice — once with interception enabled, once with the handler removed — and compare elapsed time plus the aborted/continued counts. For byte counts, listen to page.on('response') in the baseline run and sum response.buffer() lengths, or check the Network domain via a CDP session.
Mocking instead of blocking
request.respond() is the other half of the feature: instead of aborting an API call, you can answer it with deterministic data, which is valuable in tests where you don't want a live backend:
page.on('request', (req) => {
if (req.url().includes('/api/cart')) {
req.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [], total: 0 }),
});
} else {
req.continue();
}
});The contentType matters: if the page calls response.json() or parses the body, a missing or wrong content type can cause subtle failures. Verify the mock actually landed by asserting on the DOM or evaluating a fetch() against the same URL inside the page — don't assume the intercept fired just because the page loaded.
Limits and common mistakes
- Blocking stylesheets changes layout. If your selectors depend on elements being sized, positioned, or revealed by CSS (for example, a menu that only exists in the DOM after a stylesheet loads, or visibility checks via
isIntersectingViewport), abortingstylesheetrequests can break them. Drop'stylesheet'from the block list first when debugging. - URL substring filters are blunt. Blocking everything containing
"analytics"can also catch a first-party endpoint the login flow needs. Prefer host-level matching and keep the denylist short. - Aborted requests are noisy. They surface as failed requests in the console and in
page.on('requestfailed'). If your test harness asserts on console errors, filter out the resource types you intentionally block. - Interception isn't free. Every request crosses the Node/browser boundary through your callback, and enabling interception disables some HTTP cache behavior. On cache-heavy repeat visits, interception can be a net slowdown. Measure your actual workload rather than assuming blocking always wins.
- Service workers and cache. Requests served by a service worker or from cache may not pass through interception the way network requests do. If a resource you expect to block still appears, check whether a service worker is registered (
about:debugging-style inspection via CDP, orpage.evaluate(() => navigator.serviceWorker?.getRegistrations())). - Version drift. Since Puppeteer v22, the "new" headless mode is the default, so
headless: 'new'in older examples is redundant and headless/headful network behavior has largely converged. Resource-type strings and defaults can still change between versions — runnpm list puppeteerand check the release notes for your installed version before copying old snippets.
Check the result
The verification loop is simple: baseline run without interception, instrumented run with it. Compare wall-clock time to domcontentloaded (or networkidle0 if your job waits for that), the count of aborted requests by type, and — most importantly — whether the data you actually scrape is identical in both runs. If the extracted text or structured data matches and the load time dropped, the block list is safe for that site. If anything you need disappears, bisect the block list until the missing resource type or host is back.
One non-technical caveat: blocking third-party requests can skew a site's analytics and may conflict with its terms of service. For testing your own properties that's irrelevant; for scraping others, confirm it's acceptable for your use case.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.