Retina screenshots
A retina, or high-DPI, screenshot draws every CSS pixel with more than one device pixel. Text edges, icons and thin lines stay sharp on phones and modern laptops, where a 1x image looks soft. You get one with a single parameter, viewport.scale. This guide shows what each scale costs in file size, so you can choose on numbers. It is part of the capture guides.
The minimal request
Set RS_API_KEY to your key first (free sign-up).
GET:
curl -G "https://api.renderscreenshot.com/v1/screenshot" \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ -d width=1280 -d height=800 -d scale=2 \ -o [email protected]
POST:
curl -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "viewport": { "width": 1280, "height": 800, "scale": 2 }, "output": { "format": "webp", "quality": 80 } }' \ -o [email protected]
Both return an image of 2560 x 1600 pixels showing the same 1280 x 800 layout.
How scale works
viewport.scale is the device pixel ratio (DPR), the same value as window.devicePixelRatio in a browser.
| Scale | Layout width (CSS px) | Image for a 1280 x 800 viewport | Pixels per CSS pixel |
|---|---|---|---|
| 1 | 1280 | 1280 x 800 | 1 |
| 2 | 1280 | 2560 x 1600 | 4 |
| 3 | 1280 | 3840 x 2400 | 9 |
Three facts follow from this:
- The layout does not change. Media queries on
widthsee the same value. Onlyresolutionmedia queries andsrcsetchoices see the higher density. - Pixel count grows with the square of the scale. Scale 2 has 4 times the pixels, scale 3 has 9 times.
- The page may load larger images. Sites that use
srcsetorimage-set()serve their 2x assets when the scale is 2, which can add download time.
Measured: bytes and encode time by scale and format
We captured one test page (a gradient header, text, a code block, an SVG chart and three cards) at each scale and format. The capture used the same Puppeteer calls that a hosted renderer makes: setViewport with deviceScaleFactor, then screenshot with the format and quality.
| Scale | Format | Image (px) | File size | Encode time |
|---|---|---|---|---|
| 1 | PNG | 1280 x 800 | 186 KB | 84 ms |
| 1 | JPEG q80 | 1280 x 800 | 73 KB | 37 ms |
| 1 | WebP q80 | 1280 x 800 | 43 KB | 85 ms |
| 2 | PNG | 2560 x 1600 | 498 KB | 202 ms |
| 2 | JPEG q80 | 2560 x 1600 | 191 KB | 51 ms |
| 2 | WebP q80 | 2560 x 1600 | 104 KB | 205 ms |
| 3 | PNG | 3840 x 2400 | 930 KB | 372 ms |
| 3 | JPEG q80 | 3840 x 2400 | 344 KB | 78 ms |
| 3 | WebP q80 | 3840 x 2400 | 167 KB | 433 ms |
Method: Node 22.18, Puppeteer 24.10.0, Chrome 137, macOS, a static local test page loaded with waitUntil: 'load', median of 3 runs per cell, measured on 2026-10-15. File size is the byte length of the returned buffer (1 KB = 1,000 bytes). Encode time is the screenshot() call only, not navigation. Your pages compress differently (photos favour JPEG and WebP even more, flat UI favours PNG), so treat the ratios as the result, not the absolute numbers:
- Going from scale 1 to scale 2 multiplied file size by about 2.4 to 2.7, not by 4. Flat areas compress well.
- Scale 3 cost about 5 times the bytes of scale 1 in PNG and about 4 times in WebP.
- JPEG was the fastest to encode at every scale. WebP was the smallest file and about as slow to encode as PNG.
When to use each scale
| Destination | Scale | Format | Why |
|---|---|---|---|
Social cards (og_card, twitter_card) |
1 | PNG or JPEG | Platforms resize and recompress; the 1200 x 630 size is what they expect |
| Thumbnails in a dashboard or link preview | 1, then output.resize |
WebP or JPEG | Small files, many per page |
| Images in docs, blogs or app stores, viewed on phones and laptops | 2 | WebP or PNG | Sharp text on high-density screens |
| Element captures of charts, code or UI for print or zoom | 2 or 3 | PNG | Crisp lines and text when enlarged |
| Input for a vision model | 1 | PNG or JPEG | Models usually downscale large images anyway |
| Full-page captures of long pages | 1 | JPEG or WebP | Height times scale grows fast |
Device presets already include a scale (3 for iphone_14, for example). If you only need the mobile layout, use a custom viewport with scale: 1, as shown in mobile screenshots.
Serve 1x and 2x with srcset
To serve sharp images without making every visitor download the 2x file, render both and let the browser choose:
<img src="/shots/[email protected]" srcset="/shots/[email protected] 1x, /shots/[email protected] 2x" width="1280" height="800" alt="Pricing page with three plans side by side">
Set width and height to the CSS size (the 1x size). The browser reserves the space and avoids layout shift, whichever file it picks. Two requests at scale 1 and 2 are two renders and use two credits. If credits matter more than download size, render only at 2x and let 1x screens downscale.
SDK examples
Node.js
import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); for (const scale of [1, 2]) { const image = await client.take( TakeOptions.url('https://example.com').width(1280).height(800).scale(scale).format('webp').quality(80) ); await writeFile(`page@${scale}x.webp`, image); }
Python
import os from renderscreenshot import Client, TakeOptions client = Client(os.environ['RS_API_KEY']) for scale in (1, 2): image = client.take( TakeOptions.url('https://example.com') .width(1280).height(800).scale(scale).format('webp').quality(80) ) with open(f'page@{scale}x.webp', 'wb') as f: f.write(image)
Ruby
require 'renderscreenshot' client = RenderScreenshot.client(ENV.fetch('RS_API_KEY')) [1, 2].each do |scale| image = client.take( RenderScreenshot::TakeOptions .url('https://example.com') .width(1280).height(800).scale(scale).format('webp').quality(80) ) File.binwrite("page@#{scale}x.webp", image) end
PHP
use RenderScreenshot\Client; use RenderScreenshot\TakeOptions; $client = new Client(getenv('RS_API_KEY')); foreach ([1, 2] as $scale) { $image = $client->take( TakeOptions::url('https://example.com') ->width(1280)->height(800)->scale($scale)->format('webp')->quality(80) ); file_put_contents("page@{$scale}x.webp", $image); }
Java
Client client = new Client(System.getenv("RS_API_KEY")); for (double scale : new double[] {1.0, 2.0}) { byte[] image = client.take( TakeOptions.url("https://example.com") .width(1280).height(800).scale(scale).format("webp").quality(80) ); Files.write(Path.of("page@" + (int) scale + "x.webp"), image); }
Go
for _, scale := range []float64{1, 2} { image, err := client.Take(ctx, rs.URL("https://example.com"). Width(1280).Height(800).Scale(scale). Format(rs.FormatWebP).Quality(80), ) if err != nil { log.Fatal(err) } os.WriteFile(fmt.Sprintf("page@%.0fx.webp", scale), image, 0o644) }
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Image looks blurry on a phone or laptop | Rendered at scale 1 and displayed larger | Render at scale: 2 and set the CSS size to half the pixel size |
| Image is twice as large as expected on the page | Displayed at its pixel size | Set width and height attributes to the 1x size |
| Layout differs from your laptop browser | Your browser width differs from viewport.width, not the scale |
Match viewport.width; scale does not change layout |
validation_error for scale |
Value outside 1 to 3 | Use 1, 2 or 3 (fractions such as 1.5 are allowed within the range) |
| Files too large for email or chat | Scale 2 or 3 with PNG | Use JPEG or WebP, or output.resize |
| Very tall full-page image | Height multiplied by scale | Use scale 1 for full pages, or cap capture.scroll.max_height |
| Photos on the page look the same at 2x | The site serves the same image file to every density | Expected; scale sharpens what the browser draws, not the source photo |
Related
- Viewport parameters.
- Image resize to produce thumbnails from a sharp render.
- Rendering performance for how scale affects response time.
- Device pixel ratio in the glossary.