Cases / #29 · 2026-10-09 · Easy
Visual capture: full-page screenshot, element crop and PDF in one session
Four captures from one page load — viewport, full page, a single element and a PDF — rendered in the remote browser and saved on your machine, with their sizes and timings.
Run on production on 2026-10-09: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
You need visual proof of what a page showed — for monitoring, archiving, tests or a report. A viewport screenshot only covers the window; pages scroll; sometimes you want one component; sometimes a document. All of it can come from the one page the remote browser already rendered — but how big and how slow is each, once the bytes have to travel back to you?
What we used, and why
| What | Why |
|---|---|
chromium, headless: "new" | page.pdf() needs a Chromium-based browser (Chrome and Edge print headful too). |
page.screenshot() | The visible area of the viewport. |
page.screenshot({ fullPage: true }) | The whole scrollable page in one tall image. |
locator(".product_pod").first().screenshot() | Crops to one element's box — for component-level checks. |
page.pdf() | The page as a print document. |
waitUntil: "networkidle" | Images and styles have loaded before anything is captured. |
How it works
- Open books.toscrape.com and wait for the network to go quiet.
- Take a viewport screenshot, a full-page screenshot and an element screenshot of the first product card, saving each locally.
- Render the page as a PDF.
- Print the size and time of every capture.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000
// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';
import { stat, unlink, mkdir } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
const KEY = process.env.CDPFLEET_API_KEY;
const PAGE = 'https://books.toscrape.com/';
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: 'new' }),
});
if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
const dir = join(tmpdir(), `cdpfleet-captures-${Date.now()}`);
await mkdir(dir, { recursive: true });
async function capture(label, fn) {
const t = Date.now();
const path = join(dir, label.replace(/\s+/g, '-') + (label.includes('pdf') ? '.pdf' : '.png'));
await fn(path);
const { size } = await stat(path);
await unlink(path);
return { type: label, file_size_bytes: size, seconds: Math.round((Date.now() - t) / 10) / 100 };
}
try {
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
await page.goto(PAGE, { timeout: 60000, waitUntil: 'networkidle' });
const results = [];
results.push(await capture('viewport screenshot', (p) => page.screenshot({ path: p })));
results.push(await capture('full-page screenshot', (p) => page.screenshot({ path: p, fullPage: true })));
results.push(await capture('element screenshot', (p) => page.locator('.product_pod').first().screenshot({ path: p })));
results.push(await capture('pdf', (p) => page.pdf({ path: p })));
console.log(JSON.stringify({ captures: results }, null, 2));
} finally {
await browser.close();
}
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os
import tempfile
import time
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
PAGE = "https://books.toscrape.com/"
res = requests.post("https://starter.cdpfleet.com/chromium/session",
headers={"x-api-key": KEY},
json={"proxy": os.environ["PROXY_URL"], "headless": "new"}, timeout=60)
if not res.ok:
raise SystemExit(f"launch {res.status_code} {res.text}")
with sync_playwright() as p, tempfile.TemporaryDirectory() as td:
browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
try:
page = browser.new_page(viewport={"width": 1280, "height": 720})
page.goto(PAGE, timeout=60000, wait_until="networkidle")
results = []
def capture(label, fn):
ext = ".pdf" if "pdf" in label else ".png"
path = os.path.join(td, label.replace(" ", "-") + ext)
t = time.time()
fn(path)
size = os.path.getsize(path)
return {"type": label, "file_size_bytes": size, "seconds": round(time.time() - t, 2)}
results.append(capture("viewport screenshot",
lambda p: page.screenshot(path=p)))
results.append(capture("full-page screenshot",
lambda p: page.screenshot(path=p, full_page=True)))
results.append(capture("element screenshot",
lambda p: page.locator(".product_pod").first.screenshot(path=p)))
results.append(capture("pdf",
lambda p: page.pdf(path=p)))
print(json.dumps({"captures": results}, indent=2))
finally:
browser.close()
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
static final String PAGE = "https://books.toscrape.com/";
static final Gson GSON = new GsonBuilder().serializeNulls().create();
public static void main(String[] args) throws Exception {
String body = "{\"proxy\": " + GSON.toJson(System.getenv("PROXY_URL")) + ", \"headless\": \"new\"}";
HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
Path dir = Files.createTempDirectory("cdpfleet-captures");
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
Page page = browser.newPage(new Browser.NewPageOptions().setViewportSize(1280, 720));
page.navigate(PAGE, new Page.NavigateOptions().setTimeout(60000).setWaitUntil(WaitUntilState.NETWORKIDLE));
JsonArray results = new JsonArray();
long t1 = System.currentTimeMillis();
Path viewport = dir.resolve("viewport.png");
page.screenshot(new Page.ScreenshotOptions().setPath(viewport));
results.add(row("viewport screenshot", Files.size(viewport), System.currentTimeMillis() - t1));
long t2 = System.currentTimeMillis();
Path fullPage = dir.resolve("full-page.png");
page.screenshot(new Page.ScreenshotOptions().setPath(fullPage).setFullPage(true));
results.add(row("full-page screenshot", Files.size(fullPage), System.currentTimeMillis() - t2));
long t3 = System.currentTimeMillis();
Path element = dir.resolve("element.png");
page.locator(".product_pod").first().screenshot(new Locator.ScreenshotOptions().setPath(element));
results.add(row("element screenshot", Files.size(element), System.currentTimeMillis() - t3));
long t4 = System.currentTimeMillis();
Path pdf = dir.resolve("page.pdf");
page.pdf(new Page.PdfOptions().setPath(pdf));
results.add(row("pdf", Files.size(pdf), System.currentTimeMillis() - t4));
JsonObject out = new JsonObject();
out.add("captures", results);
System.out.println(new GsonBuilder().serializeNulls().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
} finally {
browser.close();
}
} finally {
for (Path f : (Iterable<Path>) Files.list(dir)::iterator) Files.deleteIfExists(f);
Files.deleteIfExists(dir);
}
}
static JsonObject row(String type, long sizeBytes, long ms) {
JsonObject o = new JsonObject();
o.addProperty("type", type);
o.addProperty("file_size_bytes", sizeBytes);
o.addProperty("seconds", Math.round(ms / 10.0) / 100.0);
return o;
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
const string PageUrl = "https://books.toscrape.com/";
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = "new" });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
var dir = Path.Combine(Path.GetTempPath(), $"cdpfleet-captures-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}");
Directory.CreateDirectory(dir);
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
var page = await browser.NewPageAsync(new() { ViewportSize = new() { Width = 1280, Height = 720 } });
await page.GotoAsync(PageUrl, new() { Timeout = 60000, WaitUntil = WaitUntilState.NetworkIdle });
var results = new JsonArray();
async Task<JsonObject> Capture(string label, Func<string, Task> fn)
{
var ext = label.Contains("pdf") ? ".pdf" : ".png";
var path = Path.Combine(dir, label.Replace(' ', '-') + ext);
var sw = Stopwatch.StartNew();
await fn(path);
var size = new FileInfo(path).Length;
File.Delete(path);
return new JsonObject
{
["type"] = label,
["file_size_bytes"] = size,
["seconds"] = Math.Round(sw.Elapsed.TotalSeconds, 2),
};
}
results.Add(await Capture("viewport screenshot",
p => page.ScreenshotAsync(new() { Path = p })));
results.Add(await Capture("full-page screenshot",
p => page.ScreenshotAsync(new() { Path = p, FullPage = true })));
results.Add(await Capture("element screenshot",
p => page.Locator(".product_pod").First.ScreenshotAsync(new() { Path = p })));
results.Add(await Capture("pdf",
p => page.PdfAsync(new() { Path = p })));
var output = new JsonObject { ["captures"] = results };
Console.WriteLine(output.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
await browser.CloseAsync();
if (Directory.Exists(dir)) Directory.Delete(dir, true);
}
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"path/filepath"
"time"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
const pageURL = "https://books.toscrape.com/"
func launch(name string, options map[string]any) (map[string]any, error) {
body, _ := json.Marshal(options)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
}
var session map[string]any
return session, json.NewDecoder(res.Body).Decode(&session)
}
type capture struct {
Type string `json:"type"`
FileSizeBytes int64 `json:"file_size_bytes"`
Seconds float64 `json:"seconds"`
}
func main() {
session, err := launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": "new"})
if err != nil {
log.Fatal(err)
}
pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
if err != nil {
log.Fatal(err)
}
defer pw.Stop()
browser, err := pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
log.Fatal(err)
}
defer browser.Close()
dir, err := os.MkdirTemp("", "cdpfleet-captures-")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(dir)
page, err := browser.NewPage(playwright.BrowserNewPageOptions{Viewport: &playwright.Size{Width: 1280, Height: 720}})
if err != nil {
log.Fatal(err)
}
if _, err := page.Goto(pageURL, playwright.PageGotoOptions{Timeout: playwright.Float(60000), WaitUntil: playwright.WaitUntilStateNetworkidle}); err != nil {
log.Fatal(err)
}
var results []capture
snap := func(label string, fn func(string)) capture {
ext := ".png"
if label == "pdf" {
ext = ".pdf"
}
path := filepath.Join(dir, label+ext)
t := time.Now()
fn(path)
info, err := os.Stat(path)
if err != nil {
log.Fatal(err)
}
return capture{Type: label, FileSizeBytes: info.Size(), Seconds: math.Round(time.Since(t).Seconds()*100) / 100}
}
results = append(results, snap("viewport screenshot", func(p string) {
if _, err := page.Screenshot(playwright.PageScreenshotOptions{Path: playwright.String(p)}); err != nil {
log.Fatal(err)
}
}))
results = append(results, snap("full-page screenshot", func(p string) {
if _, err := page.Screenshot(playwright.PageScreenshotOptions{Path: playwright.String(p), FullPage: playwright.Bool(true)}); err != nil {
log.Fatal(err)
}
}))
results = append(results, snap("element screenshot", func(p string) {
if _, err := page.Locator(".product_pod").First().Screenshot(playwright.LocatorScreenshotOptions{Path: playwright.String(p)}); err != nil {
log.Fatal(err)
}
}))
results = append(results, snap("pdf", func(p string) {
if _, err := page.PDF(playwright.PagePdfOptions{Path: playwright.String(p)}); err != nil {
log.Fatal(err)
}
}))
out := struct {
Captures []capture `json:"captures"`
}{Captures: results}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(out)
fmt.Print(buf.String())
}
What we got
| Capture | KB | Seconds |
|---|---|---|
| viewport screenshot | 139 | 0.18 |
| full-page screenshot | 654 | 0.54 |
| element screenshot | 23 | 0.16 |
| 329 | 0.18 |
From the Node.js run on 2026-10-09. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.
Raw output (Node.js)
{
"captures": [
{
"type": "viewport screenshot",
"file_size_bytes": 142800,
"seconds": 0.18
},
{
"type": "full-page screenshot",
"file_size_bytes": 669597,
"seconds": 0.54
},
{
"type": "element screenshot",
"file_size_bytes": 23401,
"seconds": 0.16
},
{
"type": "pdf",
"file_size_bytes": 337327,
"seconds": 0.18
}
]
}Takeaways
- Four captures, one page load: each is a different serialisation of the same rendered state — no re-navigation, no extra proxy traffic.
- The bytes travel back over the session's connection: a full-page PNG is several times a viewport one, so prefer element crops for repeated visual checks.
- PDF is Chromium-based browsers only — headless or headful (print to PDF across browsers); on Firefox, Camoufox or WebKit use screenshots instead.
- Wait for the network first: capturing before images arrive gives you grey boxes that look like a broken page.