When you're capturing thousands of screenshots per day, every millisecond matters. Small inefficiencies compound into significant delays and costs. This guide covers battle-tested strategies for optimizing screenshot API performance at scale.
Understanding the Latency Budget
A typical screenshot request involves multiple steps:
Step
Typical Duration
DNS resolution
10-50ms
TLS handshake
50-150ms
API processing
20-50ms
Browser launch
100-300ms
Page load
500-5000ms
Rendering
100-500ms
Image encoding
50-200ms
Response transfer
50-200ms
The page load dominates. That's where optimization has the biggest impact.
1. Minimize Page Load Time
Use the Right Wait Strategy
The default behavior waits for the page to fully load. But "fully loaded" often includes analytics scripts, ads, and other non-essential resources. Use targeted waiting instead:
// Instead of waiting for everythingconst slow =awaitfetch('/screenshots',{body:JSON.stringify({url:'https://example.com',waitUntil:'networkidle'// Waits for all network activity})});// Wait only for what mattersconst fast =awaitfetch('/screenshots',{body:JSON.stringify({url:'https://example.com',waitForSelector:'.main-content',// Your key elementwaitUntil:'domcontentloaded'})});
The difference can be 2-3 seconds per screenshot.
Identify the CSS selector for your most important content and use waitForSelector. This typically completes faster than waiting for the entire page.
Block Unnecessary Resources
Ads, trackers, and chat widgets slow things down without adding to your screenshot:
{"url":"https://example.com","blockAds":true,"blockTrackers":true,"blockResources":["font","media"]// If you don't need them}
Blocking ads alone can reduce load time by 30-50% on some sites.
2. Optimize Image Output
Choose the Right Format
Format selection affects both capture time and file size:
Format
Best For
Capture Speed
File Size
PNG
UI screenshots, transparency
Medium
Largest
JPEG
Photos, complex images
Fastest
Medium
WebP
Modern browsers
Fast
Smallest
For most applications, JPEG at quality 80 offers the best balance:
Full-page screenshots of infinitely-scrolling pages waste time and bandwidth:
// Capture just the viewport{"url":"https://example.com","fullPage":false,"width":1280,"height":800}// Or clip to a specific region{"url":"https://example.com","clip":{"x":0,"y":0,"width":1200,"height":630}}
If you need full-page captures, set a reasonable maxHeight limit to prevent infinite scroll from running forever.
3. Use Async for Batch Processing
Synchronous requests block while waiting for results. For batch processing, async endpoints are dramatically faster:
// Slow: Sequential synchronous requestsfor(const url of urls){awaitfetch('/screenshots',{body:JSON.stringify({ url })});}// Time: N * average_request_time// Better: Parallel synchronous requestsawaitPromise.all( urls.map(url=>fetch('/screenshots',{body:JSON.stringify({ url })})));// Time: max(request_times) - but limited by connection pool// Best: Submit all, poll for resultsconst jobs =awaitPromise.all( urls.map(url=>fetch('/screenshots/async',{body:JSON.stringify({ url })}).then(r=> r.json())));// Poll for completionconst results =awaitpollForResults(jobs.map(j=> j.jobId));// Time: max(processing_times) - no connection limits
The async approach scales to thousands of concurrent screenshots.
4. Implement Smart Caching
Not every screenshot needs to be fresh. Implement tiered caching:
classScreenshotCache{constructor(redis){this.redis= redis;}asyncget(url, maxAge =3600){const key =this.keyFor(url);const cached =awaitthis.redis.get(key);if(!cached)returnnull;const{ screenshot, timestamp }=JSON.parse(cached);const age =(Date.now()- timestamp)/1000;if(age > maxAge)returnnull;return screenshot;}asyncset(url, screenshot, ttl =86400){const key =this.keyFor(url);const data =JSON.stringify({ screenshot,timestamp:Date.now()});awaitthis.redis.setEx(key, ttl, data);}keyFor(url){// Normalize and hash the URLconst normalized =newURL(url).toString().toLowerCase();return`screenshot:${hash(normalized)}`;}}
Cache Invalidation Strategies
Time-based: Screenshots expire after N hours
Event-based: Invalidate when you know content changed
Version-based: Include a version parameter in URLs