The starting point
Abort the heavy requests a scraping job does not need, for every page in a browser context, then measure whether the run really moved fewer bytes.
Measure the job before you block anything
Images, fonts and video can make up much of what a page downloads, and many scraping jobs never read them. Blocking them with request interception is a common way to reduce proxy traffic. Whether it helps your job depends on your pages, so start with a measurement: run the job once as it is and record where the bytes go. scrapescope is a free, open-source local meter that reports bytes per host and per resource type for a Playwright run and models the effect of blocking images, media and fonts.
The examples below use Playwright 1.63 and the placeholder https://example.com/. Point TARGET_URL at a page you are allowed to automate, and keep proxy settings and credentials in your own launch or context options; none are needed to try the blocking itself.
Block by resource type in Python
Playwright labels every request with the resource type the rendering engine perceived: document, stylesheet, image, media, font, script, texttrack, xhr, fetch, eventsource, websocket, manifest or other. Register one route for all URLs on the browser context and abort the three heavy types; every other request continues unchanged.
Save as block_resources.py and run python block_resources.py after pip install playwright and playwright install chromium. The context also blocks service workers, for the reason explained further down.
import os
from playwright.sync_api import Route, sync_playwright
TARGET_URL = os.environ.get("TARGET_URL", "https://example.com/")
BLOCKED_TYPES = {"image", "media", "font"}
def block_heavy(route: Route) -> None:
if route.request.resource_type in BLOCKED_TYPES:
route.abort()
else:
route.continue_()
with sync_playwright() as p:
browser = p.chromium.launch()
try:
context = browser.new_context(service_workers="block")
context.route("**/*", block_heavy)
page = context.new_page()
response = page.goto(TARGET_URL, wait_until="load")
print({"status": response.status if response else None})
context.close()
finally:
browser.close()Block by resource type in Node.js
The Node.js version is the same route with the same three types. route.request().resourceType() returns the type, route.abort() fails the request before it leaves the browser, and route.continue() sends everything else as it was.
Save as block-resources.mjs and run node block-resources.mjs after npm install playwright and npx playwright install chromium.
import { chromium } from 'playwright';
const TARGET_URL = process.env.TARGET_URL ?? 'https://example.com/';
const BLOCKED_TYPES = new Set(['image', 'media', 'font']);
const browser = await chromium.launch();
try {
const context = await browser.newContext({ serviceWorkers: 'block' });
await context.route('**/*', route =>
BLOCKED_TYPES.has(route.request().resourceType()) ? route.abort() : route.continue());
const page = await context.newPage();
const response = await page.goto(TARGET_URL, { waitUntil: 'load' });
console.log({ status: response?.status() });
await context.close();
} finally {
await browser.close();
}Apply it to every page in the browser context
A route on the browser context applies to requests made by any page in that context: pages you open later, second tabs and popups a page opens with window.open(). A route on a single page applies to that page only, so a popup or a new tab would load its images and fonts again. Register the context route before the first page loads, and put it in one helper so every job uses the same rule.
When a request matches both, page routes take precedence over browser context routes, and when several page routes match, the most recently registered one wins. Keep one blocking route per context to avoid surprises.
import { chromium } from 'playwright';
const TARGET_URL = process.env.TARGET_URL ?? 'https://example.com/';
const BLOCKED_TYPES = new Set(['image', 'media', 'font']);
// One route on the context covers every page the context opens, including
// tabs opened later and popups. Register it before the first page loads.
async function blockHeavyResources(context) {
await context.route('**/*', route =>
BLOCKED_TYPES.has(route.request().resourceType()) ? route.abort() : route.continue());
}
const browser = await chromium.launch();
try {
const context = await browser.newContext({ serviceWorkers: 'block' });
await blockHeavyResources(context);
const first = await context.newPage();
const second = await context.newPage();
await Promise.all([
first.goto(TARGET_URL, { waitUntil: 'load' }),
second.goto(TARGET_URL, { waitUntil: 'load' }),
]);
console.log({ pages: context.pages().length });
await context.close();
} finally {
await browser.close();
}Block specific heavy hosts by URL pattern
Sometimes one host is the problem: a video CDN, an ad server or a font service. route() also takes a URL pattern, so you can abort everything from those hosts and leave the resource types alone. A glob such as https://media.example.net/** matches every URL on one host; a regular expression can list several. The hosts below are placeholders; take the real names from your measurement rather than from a generic blocklist.
import os
import re
from playwright.sync_api import sync_playwright
TARGET_URL = os.environ.get("TARGET_URL", "https://example.com/")
# A glob covers every URL on one host; a regular expression can cover several.
HEAVY_HOST = "https://media.example.net/**"
HEAVY_HOSTS = re.compile(r"^https://(video\.example\.org|fonts\.example\.com)/")
with sync_playwright() as p:
browser = p.chromium.launch()
try:
context = browser.new_context(service_workers="block")
context.route(HEAVY_HOST, lambda route: route.abort())
context.route(HEAVY_HOSTS, lambda route: route.abort())
page = context.new_page()
response = page.goto(TARGET_URL, wait_until="load")
print({"status": response.status if response else None})
context.close()
finally:
browser.close()Routing turns off the HTTP cache
Playwright's API reference states for both page.route() and browserContext.route(): "Enabling routing disables http cache." Once any route is registered, stylesheets, scripts and other repeated assets that the browser would normally reuse from its cache are downloaded again for each page. On a job that loads many pages in one context, that extra transfer can offset part of what blocking removes, or all of it. This is why a single-page test is not enough: measure the whole run.
Block service workers while you intercept
Requests that a page's service worker handles can bypass your routes: the API reference notes that browserContext.route() will not intercept requests intercepted by Service Worker, and recommends disabling service workers when using request interception by setting serviceWorkers to 'block'. That is the service_workers="block" (Python) and serviceWorkers: 'block' (Node.js) context option in every example above. A site that depends on a service worker, for example to cache content for offline use, can behave differently once it cannot register one.
Blocking can change what the page does
Blocking images, media and fonts can break extraction, change how a page behaves or attract anti-bot scrutiny. A page may wait for an image before rendering the data you read, lazy-load content only after media events, or treat a browser that never downloads images as unusual. Check that the values you extract are unchanged after enabling the route, and keep blocking within what the site's terms allow.
Measure again and compare
Run the same job twice through the same provider, once without the route and once with it, and compare the two runs. A second measured run is the evidence; a model or an estimate is a prediction. scrapescope can compare two reports side by side in its viewer, including bytes per host and per resource type, so you can see what the route removed and what the lost HTTP cache added back.
From reading to doing
Before you ship
- Measure the job once without blocking before changing anything.
- Register the route on the browser context, before the first page loads.
- Block service workers in the context options while routes are active.
- Confirm that the extracted values are unchanged after blocking.
- Compare a second measured run through the same provider before relying on the change.
Sources & further reading
Technical references used for this guide. Check the documentation for your installed version and your provider’s supported configuration.