Rendering performance
The time a screenshot takes is mostly the target page's time: its server, its scripts, its fonts and images. Your parameters decide how much of that you wait for, how much extra work the renderer does, and how many bytes come back. This guide ranks the settings by cost, shows a local cost model, and gives you a script to time your own pages. It is part of the scale guides.
Where the time goes
A render goes through the same stages every time:
- Browser session. Each request gets a fresh browser session in Cloudflare's browser rendering service, and the session is closed after the capture. No cookies, cache or state carry over between requests. This keeps captures independent and repeatable, and it means every render pays the session start.
- Navigation and the wait condition. The page loads until
wait.untilis met, thenwait.for_selector, thenwait.delay. - Page work. Blocking, cookie banner handling, clicks, injected scripts and styles, hidden or removed elements.
- Capture and encode. The browser draws the image and encodes it as PNG, JPEG or WebP.
- Storage and response. The file is stored, given a CDN address (
X-Cache-URL), and returned to you.
Stage 2 is usually the largest by far, and it is the one your parameters control most.
The cost of each setting
| Setting | Cost | When it pays off |
|---|---|---|
wait.until: domcontentloaded |
Saves the time images and late fonts take | Text-only or layout checks |
wait.until: load (default) |
Waits for images, styles and fonts requested during load | Most server-rendered pages |
wait.until: networkidle |
At least 500 ms extra, until the timeout on pages that never go quiet | Single-page apps you do not control |
wait.for_selector |
Ends as soon as the element appears | The most precise wait; often faster than networkidle |
wait.delay |
Exactly the time you set, capped at 10,000 ms | Only for animations |
block.ads, trackers, chat_widgets, requests, resources |
Turns on request interception: every request is checked against the lists | Saves time when the page loads many third-party resources |
block.cookie_banners |
Looks for known accept buttons, clicks the first and pauses 300 ms; otherwise hides known banner containers | When a banner would cover the content |
page.click |
Clicks, then pauses 300 ms | Opening tabs or menus before capture |
viewport.scale: 2 or 3 |
4 or 9 times the pixels to draw, encode and transfer | High-density displays |
capture.mode: full_page |
Draws the whole page height; with scroll.simulate, also scrolls step by step with scroll.delay between steps |
When you need everything below the fold |
output.format |
JPEG encodes fastest, WebP gives the smallest files | Large or many images |
A local cost model
To see the relative size of these costs, we built a test page with the problems real pages have: 12 images that each take 400 ms to arrive, an analytics script that arrives after 600 ms and sends a beacon 300 ms later that takes 900 ms, and a 6,000 px tall body. We then captured it in headless Chrome with each setting.
| Setting | Median time | Output size |
|---|---|---|
load, viewport, PNG (baseline) |
1,277 ms | 13 KB |
domcontentloaded |
670 ms | 13 KB |
networkidle |
2,475 ms | 13 KB |
load, blocking images and the analytics script |
76 ms | 16 KB |
networkidle, blocking images and the analytics script |
1,050 ms | 16 KB |
load, full page, PNG |
1,392 ms | 80 KB |
load, full page, JPEG |
1,306 ms | 63 KB |
Method: Node 22.18, Puppeteer 24.10.0, Chrome 137 on macOS, a local test server, 5 runs per setting, median, measured on 2026-10-15. These are not RenderScreenshot API latencies; they isolate what each setting does to one page. What the numbers show:
- The wait condition dominated.
networkidletook almost twice as long asload, because it waited for the late beacon and then 500 ms of silence. - Blocking slow resources removed most of the time. Blocking the images and the analytics script cut the
loadtime from 1,277 ms to 76 ms. Blocked images show as broken-image icons, which is why that output changed size. Block only what you do not need in the image. - Blocking also tamed
networkidle. With the slow requests gone, the network went quiet sooner: 1,050 ms instead of 2,475 ms. - Full page and format were small costs here because the page was simple. On real pages with photos, format matters more; see the measured table in retina screenshots.
The fastest request is the one you do not make
Every successful render is stored and returned with an X-Cache-URL header: a public CDN address for the same image, valid until X-Cache-Expires. If your users view the same screenshot many times, store that URL and serve it, instead of calling the API on every page view. The X-Cache-URL guide covers the lifetime and the limits.
For many URLs at once, the batch endpoint runs renders in parallel within your plan's concurrency limit, so a list of 50 pages does not take 50 times one render. See bulk screenshots.
A fast default profile
For most pages where you want speed without broken output:
{ "url": "https://example.com", "wait": { "until": "load", "timeout": 20 }, "block": { "ads": true, "trackers": true, "chat_widgets": true }, "viewport": { "width": 1280, "height": 800, "scale": 1 }, "output": { "format": "jpeg", "quality": 80 } }
The same profile as a GET request:
curl -G "https://api.renderscreenshot.com/v1/screenshot" \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ -d block_ads=true -d format=jpeg -d quality=80 -d timeout=20 \ -o fast.jpg
A short wait.timeout fails fast on pages that hang, so a slow site does not hold your queue for the full 30 seconds. Combine it with the retry rules in error handling.
Time your own pages
Your pages are the benchmark that matters, and a free account is enough to run this test. This script sends the same URL with each setting five times and prints the median and maximum time. It turns the cache off with cache.enabled: false so that every run is a real render. Each run is a render and uses a credit: 7 settings times 5 runs is 35 credits.
// time-variants.mjs: time the same URL under different settings (Node 22). // Usage: RS_API_KEY=... node time-variants.mjs https://your-page.example const API = process.env.RS_BASE_URL ?? 'https://api.renderscreenshot.com'; const target = process.argv[2] ?? 'https://example.com'; const RUNS = 5; const base = { url: target, cache: { enabled: false } }; // measure renders, not cache hits const variants = { 'baseline (load, png)': {}, 'domcontentloaded': { wait: { until: 'domcontentloaded' } }, 'networkidle': { wait: { until: 'networkidle' } }, 'block ads + trackers': { block: { ads: true, trackers: true } }, 'jpeg q80': { output: { format: 'jpeg', quality: 80 } }, 'scale 2': { viewport: { scale: 2 } }, 'full page': { capture: { mode: 'full_page' } }, }; const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)]; for (const [name, extra] of Object.entries(variants)) { const times = []; let bytes = 0; for (let i = 0; i < RUNS; i++) { const t0 = performance.now(); const res = await fetch(`${API}/v1/screenshot`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ ...base, ...extra }), }); const body = Buffer.from(await res.arrayBuffer()); if (!res.ok) throw new Error(`${name}: HTTP ${res.status} ${body.toString().slice(0, 200)}`); times.push(performance.now() - t0); bytes = body.length; } console.log(`${name.padEnd(24)} median ${Math.round(median(times))} ms max ${Math.round(Math.max(...times))} ms ${bytes} bytes`); }
The script was checked on 2026-10-15 with Node 22.18 against a local mock of the API; the timings it prints come from your own run. Run it from the region where your servers are, at the time of day your jobs run, and look at the maximum as well as the median. Variation between runs is normal: a page's own servers and third-party scripts change speed through the day.
SDK examples: the fast profile
Node.js
import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const image = await client.take( TakeOptions.url('https://example.com') .waitFor('load') .blockAds() .blockTrackers() .blockChatWidgets() .format('jpeg') .quality(80) );
Python
import os from renderscreenshot import Client, TakeOptions client = Client(os.environ['RS_API_KEY']) image = client.take( TakeOptions.url('https://example.com') .wait_for('load') .block_ads() .block_trackers() .block_chat_widgets() .format('jpeg') .quality(80) )
Ruby
require 'renderscreenshot' client = RenderScreenshot.client(ENV.fetch('RS_API_KEY')) image = client.take( RenderScreenshot::TakeOptions .url('https://example.com') .wait_for('load') .block_ads .block_trackers .block_chat_widgets .format('jpeg') .quality(80) )
PHP
use RenderScreenshot\Client; use RenderScreenshot\TakeOptions; $client = new Client(getenv('RS_API_KEY')); $image = $client->take( TakeOptions::url('https://example.com') ->waitFor('load') ->blockAds() ->blockTrackers() ->blockChatWidgets() ->format('jpeg') ->quality(80) );
Java
Client client = new Client(System.getenv("RS_API_KEY")); byte[] image = client.take( TakeOptions.url("https://example.com") .waitFor("load") .blockAds() .blockTrackers() .blockChatWidgets() .format("jpeg") .quality(80) );
Go
image, err := client.Take(ctx, rs.URL("https://example.com"). WaitFor(rs.WaitLoad). BlockAds(). BlockTrackers(). BlockChatWidgets(). Format(rs.FormatJPEG). Quality(80), )
Troubleshooting slow captures
| Symptom | Likely cause | Fix |
|---|---|---|
| Every capture of one site takes close to 30 s, then fails | networkidle never reached (polling, websockets, live chat) |
Use wait.for_selector, or block the polling hosts with block.requests |
| Captures are slow but succeed | Many third-party scripts and images | block.ads, block.trackers, block.chat_widgets |
Slow only with full_page and scroll.simulate |
Many scroll steps times scroll.delay |
Raise scroll.step, lower scroll.delay, or cap scroll.max_height |
| Large responses take long to download | Scale 2 or 3, PNG, full page | JPEG or WebP, scale 1, or output.resize |
| A fixed extra second on every request | wait.delay left in from debugging |
Remove it or replace it with a selector wait |
| Same image requested again and again | No reuse of results | Serve X-Cache-URL until X-Cache-Expires |
| Queue backs up behind a few slow sites | One slow page holds a worker for the full timeout | Lower wait.timeout and retry later, see error handling |
Related
- Wait strategies for choosing the right condition.
- Blocking parameters and the ad and cookie banner guide.
- Retina screenshots for measured format and scale costs.