· Tutorials · By Simon Chiu

Convert a Webpage to an Image: 5 Methods, Measured

Convert a webpage to a PNG, JPEG or WebP five ways, from a DevTools command to an API. Effort, cost and timings for each, plus the settings that matter.

To convert a webpage to an image, you load the page in a real browser engine and save what it paints. There are five practical ways to do that. They range from a menu command with no setup to an HTTP call that runs at any volume. If you need one image right now, use Chrome's Capture full size screenshot command. If you need the same image every day, or for every user, use a script or an API. Here is the one-line version of the scripted path:

npx playwright screenshot --full-page --viewport-size=1280,800 https://example.com page.png

This post ranks the five methods by the effort they take and what they cost to keep running. It times each on the same page, and lists the settings that make two captures of one page match. For scripted capture with Puppeteer, Playwright or Selenium, see how to take website screenshots with code. For rendering your own HTML instead of a live URL, see the HTML to image API overview, which is the hub for this topic.

Tested on 2026-10-18 with Node 22.18.0, [email protected] (Chromium headless shell), [email protected], Google Chrome 155.0.8059.80, macOS 27.0.1 on an Apple M1 Pro, against https://example.com. The RenderScreenshot requests on this page follow the API reference but were not part of the timed run.

The five methods at a glance

Method Setup Repeatable? Full page Runs where Cost to keep running
1. Browser command (DevTools) None No, manual Yes Your desktop Your time per image
2. Chrome --screenshot flag Chrome installed Yes No, viewport only Any machine with Chrome Low; breaks quietly
3. Playwright (CLI or script) npm i playwright Yes Yes Your server or CI A browser per worker
4. Puppeteer script npm i puppeteer Yes Yes Your server or CI A browser per worker
5. Screenshot API An API key Yes Yes Someone else's browsers Per image

The ranking below goes from least to most setup. The right answer depends on one question: who runs the browser after today?

What we measured

Each method captured https://example.com at a 1280 x 800 viewport. "Cold" means a fresh browser process for each image, which is what a cron job or a CLI call does. "Warm" means one browser and one page that stay open between captures. Each figure is the median of five runs, with the first warm run discarded so DNS and TLS are already set up.

Method Median time per image Notes
Chrome --headless --screenshot, cold 4,323 ms Full Chrome app binary, fresh --user-data-dir each run
Puppeteer, cold 0.84 to 4.76 s Wide spread between batches; the very first run after install took 14.7 s
Playwright script, cold 1,134 ms Uses the Chromium headless shell
npx playwright screenshot, cold 1,360 ms One run, includes npx start-up
Playwright, warm (page reused) 48 ms goto plus screenshot only

Two lessons came out of this. First, almost all the time in a cold capture is starting the browser, not drawing the page. Keeping one browser open made the same capture about 20 times faster. Second, cold start-up times on a laptop vary a lot. The first Puppeteer run after installing a new Chrome build was 14.7 seconds, and later batches ranged from under one second to almost five. Do not size a production system from a single cold timing.

Output size depends on the format far more than on the method:

Format (1280 x 800, example.com) Bytes
PNG (Playwright) 80,922
PNG (Chrome --screenshot) 81,121
JPEG, quality 80 59,494
WebP, quality 80 35,722

The scripts that produced these numbers are below. They exit with a non-zero code when the page fails, so a broken capture never passes as a success.

Method 1: a browser command, for one image

Every desktop browser can save a page as an image without an extension. In Chrome or Edge, open DevTools, press Cmd+Shift+P (Ctrl+Shift+P on Windows and Linux), type "screenshot" and pick Capture full size screenshot. In Firefox, right-click the page and choose Take Screenshot, then Save full page.

This is the right method when a person needs one image and will look at it before using it. It is the wrong method for anything you repeat, because the result depends on your window size, your zoom level, your extensions and whether you are logged in. Our guide to full page screenshots in Chrome covers the traps of this command on long pages.

Method 2: Chrome's --screenshot flag

Chrome has a built-in capture flag. It needs no Node and no Python:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless --screenshot="$PWD/page.png" \
  --window-size=1280,800 --hide-scrollbars \
  https://example.com

On Linux, the binary is usually google-chrome or chromium. Three things to know before you script it:

  • It captures the window only. There is no full-page option; set a tall --window-size if you need more of the page.
  • It has no wait option. It captures when the page has loaded. Content that arrives later from JavaScript can be missing.
  • Exit codes are not a reliable success signal. In our runs, Chrome sometimes exited with status 2 even though it had written the file, and printed display errors on every run. Check that the file exists and is not empty instead.

Use a fresh --user-data-dir per run if you run captures in parallel; two Chrome processes that share a profile get in each other's way. The command-line screenshot guide compares this flag with other CLIs in more depth.

Method 3: Playwright, from the CLI or a script

Playwright ships a screenshot command, which is the shortest path from a URL to a full-page image:

npm i -D [email protected]
npx playwright install chromium
npx playwright screenshot --full-page --viewport-size=1280,800 \
  https://example.com page.png

The CLI also takes --color-scheme, --device, --lang and --wait-for-timeout. It lists a --wait-for-selector option too, but in our run --wait-for-selector="h1" on example.com printed "Waiting for selector h1..." and had not returned after two minutes, so we use a script for selector waits. Once you need retries, cookies or many URLs, move to a script. This one captures a full page and fails loudly:

// pw-shot.mjs — usage: node pw-shot.mjs <url> <out.png>
import { chromium } from 'playwright';

const [url = 'https://example.com', out = 'page.png'] = process.argv.slice(2);
const browser = await chromium.launch();
try {
  const context = await browser.newContext({
    viewport: { width: 1280, height: 800 },
    deviceScaleFactor: 1,
  });
  const page = await context.newPage();
  const res = await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
  if (!res?.ok()) throw new Error(`HTTP ${res?.status()} for ${url}`);
  await page.screenshot({ path: out, fullPage: true });
} finally {
  await browser.close();
}

An unhandled error in an ES module makes Node exit with status 1, so a shell loop or CI job sees the failure. For many URLs, launch the browser once, open one page per worker and reuse them; that is the 48 ms row in the table above. Our Playwright screenshot tutorial covers waits, element captures and authentication in detail.

Method 4: Puppeteer

Puppeteer does the same job with Chrome's own automation library. The script is almost the same shape:

// pp-shot.mjs — usage: node pp-shot.mjs <url> <out.png>
import puppeteer from 'puppeteer';

const [url = 'https://example.com', out = 'page.png'] = process.argv.slice(2);
const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
  const res = await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
  if (!res || !res.ok()) throw new Error(`HTTP ${res?.status()} for ${url}`);
  await page.screenshot({ path: out, fullPage: true });
} finally {
  await browser.close();
}

Choose Puppeteer if your team already uses it, or if you need Chrome DevTools Protocol features directly. Choose Playwright if you also want the CLI above, Firefox or WebKit, or the Python port, which has the same API in synchronous and asynchronous form. Either library, run on your own servers, leaves you in charge of the browser binary, system fonts, memory and stuck processes. The post on Puppeteer timeouts and memory leaks shows what that looks like after a few weeks in production.

Method 5: a screenshot API

With an API, your code sends the URL and settings over HTTPS, and the provider runs the browser. This RenderScreenshot request asks for a full-page WebP at twice the pixel density, with cookie banners blocked:

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 },
    "capture": { "mode": "full_page" },
    "output": { "format": "webp", "quality": 80 },
    "block": { "cookie_banners": true }
  }' \
  -o page.webp

The same request with the Node.js SDK:

import { writeFile } from 'node:fs/promises';
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);
const image = await client.take(
  TakeOptions.url('https://example.com')
    .width(1280)
    .height(800)
    .scale(2)
    .fullPage()
    .format('webp')
    .quality(80)
    .blockCookieBanners()
);
await writeFile('page.webp', image);

The response is the image itself. It also carries an X-Cache-URL header, a CDN link to the same image that needs no API key, and X-Screenshot-Title and X-Screenshot-Description headers taken from the real page. Those are useful when you store the image next to a link preview. All parameters are listed in the GET and POST references.

Every successful render uses one credit, and failed renders use none. The Free plan includes 150 credits a month, and the Starter plan is $10 for 2,500. The trade is plain: you pay per image instead of running browsers, and you lose the ability to run arbitrary code against the page between steps.

Settings that make two captures match

Most "the image looks wrong" reports are not bugs in the tool. Two captures of one page differ because one of these settings differs:

Setting Why it changes the image What to fix it to
Viewport width Responsive CSS picks a different layout A fixed width, such as 1280 or 1440
Device pixel ratio 1x looks soft on high-density screens; 2x doubles each dimension 1 for archives and diffs, 2 for display
Colour scheme Sites that follow prefers-color-scheme switch theme Set it explicitly, light or dark
Wait condition Early captures miss fonts, data and lazy images load at least; a selector for app pages
Locale and time zone Dates, currencies and language change Set both explicitly
Cookie banners They cover the content on first visit Block them, or set the consent cookie

The wait condition matters most for single-page apps. A fixed delay is either too short on a slow day or wasted time on a fast one. Wait for an element that only exists when the content is ready. The wait strategies guide has a decision table for the API, and the same ideas apply to Playwright's waitForSelector.

Which method should you use?

  • One image, once: the browser command (method 1).
  • A few images in a shell script, no full page needed: Chrome's flag (method 2), with a file-exists check.
  • Full pages from CI, or tests that already use Playwright: the Playwright CLI or script (method 3).
  • An existing Puppeteer codebase: keep Puppeteer (method 4) and reuse the browser.
  • Images inside your product, for your users, at any volume: an API (method 5), so that browser start-up, fonts and memory are not your problem.

If you are building a feature where users download a page from your app as an image, the export to image use case shows how to pass the user's session and capture one element.

Troubleshooting

Symptom Cause Fix
Image is the wrong width or layout Default viewport differs per tool Set the viewport explicitly in every method
Text is blurry Captured at device pixel ratio 1 Use deviceScaleFactor: 2 or viewport.scale: 2
Only the top of the page Viewport capture, not full page fullPage: true, --full-page or capture.mode: full_page
Images lower on the page are blank Lazy loading never triggered Scroll before capture, or use capture.scroll.simulate in the API
Fallback font in the image Capture before web fonts loaded Wait for load at least; avoid domcontentloaded
Cookie banner covers the page First visit, no consent cookie Block banners or set the consent cookie before navigation
Chrome CLI exits with status 2 but wrote a file Chrome process quirks on desktop builds Check the output file, not only the exit code
Every capture takes seconds Browser launched per image Keep one browser open and reuse pages
Script reports success on a 404 page Status code not checked Check response.ok() and throw

Summary

Converting a webpage to an image is easy once and hard to do the same way every time. Pick the method by who runs the browser after today. If the answer is "my product, for my users", create a free account and try the API request above on your own URL.

Related in this topic