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, againsthttps://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-sizeif 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
- HTML to Image API: Render HTML and CSS to PNG Send HTML and CSS, get a PNG, JPEG, WebP or PDF back from real Chrome. Base URLs for assets, web fonts, transparent backgrounds and signed URLs.
- Best HTML to Image APIs (2026): 9 Options Compared The best HTML to image APIs compared: rendering engine, ready signals, fonts, delivery, public links and prices at three volumes, from each vendor's own docs.
- html2canvas Alternative: Render the DOM in Real Chromium We tested html2canvas 1.4.1 against real Chromium on 14 CSS features. See what it drops, when it is still the right tool, and how to move capture server-side.
- HTML to Image in Node.js: 4 Ways, Same 6 Templates HTML to image in Node.js with Puppeteer, node-html-to-image, satori and an API, run on the same six templates. Measured times, what broke, and code that works.
- HTML to Image in Python: imgkit, html2image, Playwright Convert HTML to an image in Python with html2image, Playwright or an API, compared on five templates with timings and renders, and why imgkit is a dead end.
- Render HTML to PNG: 19 CSS Probes, 4 Renderers Render HTML to PNG with a browser library, satori, headless Chrome or an API. We scored 19 CSS features per renderer with pixel probes. Code and results.