ScreenshotNeo

BlogHow-to

Convert an HTML Page to AVIF with a Headless Browser

Render a page with Chrome or Playwright, then encode the screenshot as AVIF with Sharp. Includes runnable code, options, and troubleshooting.

By the ScreenshotNeo team4 October 202610 min read

To convert an HTML page to AVIF with a headless browser, render the page and capture it as a screenshot, then encode the screenshot bytes with an AVIF-capable image library such as Sharp. The documented Chrome headless screenshot command writes PNG, and Playwright’s screenshot API documents PNG, JPEG, and WebP output; neither interface documents direct AVIF screenshot output. The reliable documented workflow is therefore render → capture → encode.

This guide uses Node.js, Playwright, and Sharp. It also includes a Chrome CLI route, Python and cURL examples for remote HTML, capture choices, encoder settings, and fixes for common problems.

1. Install the tools

Use a supported Node.js installation, then create a project and install Playwright and Sharp. Playwright’s browser installation command downloads Chromium for its automation runtime.

mkdir html-to-avif
cd html-to-avif
npm init -y
npm install playwright sharp
npx playwright install chromium

For reproducible output, keep the Node.js, Playwright, browser, and Sharp versions fixed in your project lockfile and deployment environment. Browser rendering can vary with the operating system, browser version, settings, hardware, power source, and headless mode. See Playwright’s visual comparison guidance.

2. Capture a page and encode it as AVIF with Playwright

Save the following as capture.mjs. It navigates to the URL, waits for the page load event, captures either the viewport or full page, then passes the PNG buffer to Sharp’s AVIF encoder.

import { chromium } from 'playwright';
import sharp from 'sharp';

const url = process.argv[2] ?? 'https://example.com';
const output = process.argv[3] ?? 'page.avif';
const fullPage = process.env.FULL_PAGE === '1';

const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage({
    viewport: { width: 1440, height: 900 },
    deviceScaleFactor: 1,
  });

  await page.goto(url, {
    waitUntil: 'load',
    timeout: 60_000,
  });

  const png = await page.screenshot({
    type: 'png',
    fullPage,
    animations: 'disabled',
  });

  await sharp(png)
    .avif({ quality: 55, effort: 4 })
    .toFile(output);

  console.log(`Saved ${output}`);
} finally {
  await browser.close();
}

Run a viewport capture with node capture.mjs https://example.com page.avif. For a full-page capture, use FULL_PAGE=1 node capture.mjs https://example.com page.avif.

The example’s navigation wait is appropriate for many static pages. Applications that render data after the load event may need an application-specific readiness condition, described below. Avoid assuming that networkidle always means a page is visually ready: analytics, polling, and long-lived requests can prevent network quiet.

Can Playwright save a screenshot as AVIF?

The documented Playwright Page API screenshot types are PNG, JPEG, and WebP. Capture a supported format to a buffer, then encode it with Sharp. The buffer path avoids writing an intermediate screenshot file. See Playwright’s screenshot API and Sharp’s AVIF output options.

3. Choose what to capture

Need Playwright setting Notes
Visible viewport fullPage: false (default) Captures the current viewport dimensions.
Entire scrollable page fullPage: true Captures beyond the viewport. Very tall pages can produce large images and use more memory.
One component locator(selector).screenshot() Useful for a chart, card, or other selected element. Make sure the locator resolves to the intended element.
Higher pixel density deviceScaleFactor: 2 or scale: 'device' Device scale affects output pixels; full-page extent is a separate choice. Higher scale increases pixel count and processing needs.

Playwright documents fullPage, element screenshots, and CSS-pixel versus device-pixel scale in its screenshot guide and Page API. For example, replace the screenshot call with an element capture:

const chart = page.locator('#chart');
await chart.waitFor({ state: 'visible' });
const png = await chart.screenshot({ type: 'png' });
await sharp(png).avif({ quality: 55, effort: 4 }).toFile('chart.avif');

For lazy-loaded images, scrolling the page before capture can trigger loading. Do so only when the page’s behavior requires it; there is no universal scroll strategy for every site.

4. Set AVIF quality and output behavior

Sharp documents AVIF output with quality, lossless, effort, chroma subsampling, and bit-depth controls. The defaults and available settings can depend on the installed Sharp/libvips build, so consult the docs for the version you install.

Option What it controls
quality Integer from 1–100; documented default is 50. Higher values generally preserve more visual detail, with possible increases in file size and encoding work.
lossless Requests lossless AVIF encoding. File size and processing cost can differ substantially from lossy output.
effort Integer from 0–9; documented default is 4. Higher effort can require more encoding time. Measure for your image set.
chromaSubsampling Controls chroma sampling; the documented default is 4:2:0. Fine colored text and UI edges may benefit from trying a different setting.
bitdepth Controls AVIF bit depth where supported by the encoder build.
await sharp(png)
  .avif({
    quality: 60,
    effort: 5,
    chromaSubsampling: '4:4:4',
  })
  .toFile('page.avif');

These are encoder controls, not promises of a particular file size, visual quality, or speed. Compare representative captures at the dimensions and content types you expect. Sharp can also return encoded bytes instead of a file:

const { data, info } = await sharp(png)
  .avif({ quality: 55, effort: 4 })
  .toBuffer();

console.log(`AVIF: ${info.width}×${info.height}, ${data.length} bytes`);

When writing with toFile, Sharp infers output format from the file extension if no explicit output format is selected; its output documentation lists AVIF among the supported inferred formats. For clarity, this guide calls .avif() explicitly. See Sharp output options.

5. Use Chrome’s headless CLI for a simple capture

For a one-off viewport screenshot, Chrome’s headless command-line interface can render and save a PNG. The documented --screenshot flag writes screenshot.png; convert that file with Sharp in a second step.

chrome --headless --window-size=1440,900 --screenshot=page.png https://example.com

Then, in a project where Sharp is installed, save this as encode.mjs and run node encode.mjs:

import sharp from 'sharp';

await sharp('page.png')
  .avif({ quality: 55, effort: 4 })
  .toFile('page.avif');

Chrome’s documented screenshot route is convenient for a basic capture. Playwright gives more control over navigation, page state, element selection, full-page extent, and buffer handling. See the Chrome Headless command-line reference.

6. Convert a local HTML file or remote HTML response

To capture a local file, navigate to its absolute file URL. Local pages that depend on relative asset paths should remain in their original directory structure so the browser can load those resources.

const { pathToFileURL } = await import('node:url');
const fileUrl = pathToFileURL('/absolute/path/to/page.html').href;
await page.goto(fileUrl, { waitUntil: 'load' });

If you have an HTML string rather than a file, load it directly into the page. External scripts, stylesheets, fonts, and images still need to be reachable from the browser context.

await page.setContent(`
  <!doctype html>
  <html>
    <head><style>body { font: 24px sans-serif; }</style></head>
    <body><h1>Rendered HTML</h1></body>
  </html>
`, { waitUntil: 'load' });

Python: fetch HTML, render with Playwright, encode with Sharp

Python Playwright can render and capture the page, but this example delegates AVIF encoding to the installed Sharp command above. Write the capture as PNG, then invoke a small Node.js encoder. This keeps the AVIF step on the documented Sharp API.

# capture.py
import asyncio
from pathlib import Path
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page(viewport={"width": 1440, "height": 900})
        await page.goto("https://example.com", wait_until="load", timeout=60_000)
        png = await page.screenshot(full_page=False, type="png")
        Path("page.png").write_bytes(png)
        await browser.close()

asyncio.run(main())

Install Python Playwright and its browser, then run the capture and encoder:

python -m pip install playwright
python -m playwright install chromium
python capture.py
node encode.mjs

cURL: retrieve HTML, then render it in a browser

cURL can download an HTML response, but it does not execute JavaScript or render CSS. Save a response with:

curl -L --fail --show-error https://example.com -o page.html

Then open the local file with Chrome and encode the resulting PNG using Sharp:

chrome --headless --screenshot=page.png file:///absolute/path/to/page.html
node encode.mjs

Use the cURL step only when downloading the source HTML is useful. If the page relies on relative assets or client-side code, navigating directly to the remote URL with Playwright is usually a more faithful route.

7. Wait for the page to be ready

Navigation completion does not guarantee that an application has finished rendering the content you want. For a known page, wait for a meaningful selector or state:

await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.locator('[data-capture-ready="true"]').waitFor({
  state: 'visible',
  timeout: 15_000,
});
await page.screenshot({ path: 'page.png' });

If you need a fixed delay for a known animation or delayed component, use a short, deliberate delay rather than an arbitrary long wait:

await page.waitForTimeout(1_000);

Chrome’s headless documentation explains that it parses HTML and runs scripts that can alter the DOM before --dump-dom serializes it; browser rendering likewise matters when the screenshot must reflect JavaScript-driven page state. See Chrome Headless.

8. Troubleshooting

Symptom Likely cause Fix
AVIF encoder reports an unsupported format or missing codec The Sharp/libvips installation lacks the expected AVIF support, or the input/output call is incorrect. Check the installed Sharp version and its AVIF docs; use .avif() explicitly and reinstall using the package’s supported install method for your platform.
Screenshot is blank or missing page content Capture happened before client-side rendering, the target is blocked, or navigation failed. Inspect the navigation response and console, wait for a page-specific visible selector, and verify the URL opens in the same browser environment.
Fonts, images, or styles are missing Relative resources cannot resolve from a local file, network requests failed, or the page has not finished loading them. Keep assets alongside the HTML with paths intact, use the hosted page URL where appropriate, and wait for the relevant resources or selector.
Navigation times out The site keeps network connections open, is slow, or never reaches the chosen lifecycle event. Try domcontentloaded and then wait for the specific content needed. Set a bounded timeout and check for access restrictions.
Full-page image is unexpectedly tall or memory-heavy The page has long or infinite-scroll content; output pixels grow with page extent and device scale. Capture a specific element or viewport, lower device scale, or use a page-specific capture boundary.
Text or icons look soft Low device scale or lossy chroma subsampling can affect fine edges. Try a higher device scale and compare AVIF quality or chroma subsampling options on representative content.
Repeated captures differ Dynamic content, animation, timestamps, browser version, OS, or other rendering conditions changed. Disable animations where appropriate, stabilize page data, and keep the browser/runtime environment consistent. Playwright documents these sources of variation in its visual comparison guidance.
Chrome command is not found The executable name or PATH differs on the operating system. Use the installed Chrome binary’s full path, or use Playwright’s bundled Chromium after installing it.

9. Performance, reliability, and cost considerations

  • Separate the work: page navigation and rendering happen in the browser; AVIF encoding happens afterward. Either stage can dominate runtime depending on the page and chosen settings, so measure your own workload rather than assuming a faster route.
  • Bound resource use: cap navigation and selector wait times, avoid unnecessary full-page captures, and consider lower device scale for large pages. Pixel dimensions grow with capture extent and scale.
  • Reuse judiciously: for repeated jobs, browser startup overhead may be reduced by keeping a browser process available and opening a fresh page per capture. Ensure failures close pages and browsers and isolate sessions when handling unrelated sites or credentials.
  • Make output deterministic enough for your use: pin runtime versions, stabilize dynamic page content, wait on explicit readiness, and control viewport and device scale. Browser environment differences can still change pixels.
  • Choose encoder effort deliberately: AVIF quality and effort are controls, not performance guarantees. Test the quality/size/time tradeoff on representative captures.
  • Budget both stages: self-hosted browser rendering consumes compute and memory, while image encoding consumes CPU and memory. The research sources provide no benchmark or universal cost figure.

Or skip the browser setup

ScreenshotNeo is a website screenshot API. One GET request returns a screenshot in PNG, JPEG, or WebP, or a PDF; encode a returned image as AVIF with Sharp if AVIF is the required output. ScreenshotNeo’s API response does not claim direct AVIF output. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Encode the returned image with Sharp using the settings above when you need AVIF.

Sign up free for 1,000 screenshots a month, with no card required.

FAQ

Does the browser need to support AVIF?

No. The browser captures a supported screenshot format; Sharp encodes those pixels as AVIF afterward.

Does converting to AVIF preserve selectable text?

No. A screenshot is a raster image of rendered pixels, so page text is not retained as text.

Can I use this for an HTML string that is not hosted?

Yes. Use Playwright’s page.setContent(), and ensure any referenced external assets are accessible to the browser.

Will AVIF always be smaller than PNG or WebP?

No fixed size outcome is guaranteed. Compare formats and settings on the pages and visual quality requirements that matter to your application.