Blocking Images and Fonts with Puppeteer Request Interception: When It's Worth It
Puppeteer's request interception can block images, fonts, and trackers to cut bandwidth and runtime in scraping and CI — if you resolve every request and measure the actual savings.
26 Jan 2026, 02:26 UTC

You've got a Puppeteer job that loads a few thousand pages — maybe a scraper, maybe a visual-diff or smoke-test suite in CI. Most of the runtime isn't your code; it's the browser dutifully downloading hero images, webfonts, and analytics beacons you will never look at. Request interception is the supported way to stop that work before it happens, and it's one of the cheapest wins in Puppeteer engineering — provided you handle every request and accept the trade-offs.
The core mechanism
Call page.setRequestInterception(true) and every network request the page makes fires a request event. Your handler must call exactly one of three methods on each request: request.abort() to block it, request.continue() to let it through, or request.respond() to stub a response. Miss one — for example, an early return in a branch — and that request hangs forever, which is the single most common cause of flaky interception scripts.
A worked example: block heavy assets, count what you saved
Run this with Node.js (18+ is a safe baseline) in a project where Puppeteer is installed. No special permissions are needed beyond normal network access.
const puppeteer = require('puppeteer');
async function loadWithBlocking(url) {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
const blocked = {};
let intercepted = 0, handled = 0;
await page.setRequestInterception(true);
page.on('request', (req) => {
intercepted++;
const type = req.resourceType();
if (['image', 'font', 'media'].includes(type)) {
blocked[type] = (blocked[type] || 0) + 1;
handled++;
req.abort();
} else {
handled++;
req.continue();
}
});
const start = Date.now();
await page.goto(url, { waitUntil: 'load', timeout: 60000 });
const elapsed = Date.now() - start;
console.log({ elapsed, blocked, intercepted, handled });
if (intercepted !== handled) {
throw new Error('Some requests were never resolved — check handler branches');
}
await browser.close();
}
loadWithBlocking('https://example.com');
Two things to notice. First, the intercepted === handled assertion: it turns the classic "forgot to continue a request" bug into a loud failure instead of a mysterious timeout. Second, the comparison that actually justifies this technique is missing until you add it: run the same page without interception and compare elapsed and the blocked-request counts. On image-heavy pages the difference is usually dramatic; on a lean API-driven page it may be negligible.
Block by URL pattern when asset-type blocking is too blunt
Some pages gate rendering on image or font load events — lazy-loaded content is the usual suspect — so blocking image outright can produce false negatives in tests or missing data in scrapes. A safer variant targets third-party trackers and known ad/analytics hosts by URL, leaving first-party assets alone:
const BLOCKED_HOSTS = ['google-analytics.com', 'googletagmanager.com', 'doubleclick.net'];
page.on('request', (req) => {
const host = new URL(req.url()).hostname;
if (BLOCKED_HOSTS.some((h) => host.endsWith(h))) {
req.abort();
} else {
req.continue();
}
});
This trims bandwidth and cookie-consent noise without changing what the page renders, which is usually what a test suite actually wants.
Stubbing instead of blocking
The same hook supports request.respond(), which is arguably more valuable for tests than blocking: you can answer API calls with fixed JSON and make end-to-end tests deterministic, independent of a live backend. The cost is that you're now maintaining fake responses that can drift from the real API — treat them as fixtures under version control, not throwaway code.
Trade-offs and limitations
- Per-request overhead: interception routes every request through your Node process. If you're not blocking or stubbing much, you pay latency for no benefit. Enable it only where the savings are measurable.
- Service workers and cache: resources served from a service worker or the HTTP cache may not fire interceptable requests at all, so your blocking can silently not apply. Verify with the request counter, not by assumption.
- Rendering differences: blocked fonts change text metrics; blocked images change layout and lazy-load triggers. If your test asserts on layout, block trackers by host rather than asset types.
- Version sensitivity: Puppeteer's headless defaults and network APIs have changed across releases. Check the changelog for your installed version before relying on exact behavior, and pin the version in CI.
- Scraping etiquette: blocking assets reduces load on the target server, but automated access itself may still violate terms of service. Confirm permission before running at scale.
Closing: measure before and after
The actionable step isn't just "turn on interception" — it's the baseline comparison. Wrap your page load in a timer, count aborted requests by resourceType(), and run the same URL set with interception off. If blocked bytes and wall-clock time drop meaningfully and your assertions still pass, keep it. If the page breaks or the savings are trivial, interception is overhead you don't need. The counter assertion (intercepted === handled) should stay in the script permanently; it's your early warning for the hang-everything bug.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.