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:

  1. 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.
  2. Navigation and the wait condition. The page loads until wait.until is met, then wait.for_selector, then wait.delay.
  3. Page work. Blocking, cookie banner handling, clicks, injected scripts and styles, hidden or removed elements.
  4. Capture and encode. The browser draws the image and encodes it as PNG, JPEG or WebP.
  5. 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. networkidle took almost twice as long as load, 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 load time 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 in this topic

Was this page helpful?