[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #29 · 2026-10-09 · Example · Easy · for [monitoring](https://cdpfleet.com/monitoring), [testing](https://cdpfleet.com/cross-browser-testing)

# 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.

[Chromium](https://cdpfleet.com/docs/browsers/chromium)

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

1.  Open books.toscrape.com and wait for the network to go quiet.
2.  Take a viewport screenshot, a full-page screenshot and an element screenshot of the first product card, saving each locally.
3.  Render the page as a PDF.
4.  Print the size and time of every capture.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/29-screenshot-and-pdf), 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:pass@proxy.example.com:8000`

 

```node
// npm install playwright@1.60.0
// 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();
}
```

```python
# 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()
```

```java
// 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;
  }
}
```

```csharp
// 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
// go get github.com/playwright-community/playwright-go@v0.6000.0
// 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 |
| pdf | 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](https://cdpfleet.com/docs/cases/print-to-pdf)); 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.

---
Source: https://cdpfleet.com/docs/cases/screenshot-and-pdf · Markdown: https://cdpfleet.com/docs/cases/screenshot-and-pdf.md · All docs for AI tools: https://cdpfleet.com/llms.txt
