ScreenshotNeo

BlogHow-to

How to Take a Puppeteer Screenshot of a Page with WebGL Content

Capture WebGL pages reliably with Puppeteer: wait for the scene to render, choose the right headless mode, and diagnose blank or stale screenshots.

By the ScreenshotNeo team4 October 20268 min read

Use Puppeteer’s Page.screenshot() after the page loads and the WebGL application signals that its scene is ready. Navigation completing—even with networkidle2—does not guarantee that a WebGL scene has rendered. Start with Puppeteer’s default headless mode; if you use headless: 'shell', add --enable-gpu when GPU acceleration is needed, and confirm the host has suitable graphics support.

1. Install Puppeteer and capture after the scene is ready

This runnable example uses a placeholder readiness flag. Replace the URL and readiness condition with the ones for your page. The page must expose or otherwise let you identify a reliable signal, such as a global flag, a DOM marker, or an application-controlled render-complete event.

npm install puppeteer
// screenshot-webgl.mjs
import puppeteer from 'puppeteer';

const url = 'https://example.com/webgl-page';
const browser = await puppeteer.launch();

try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000 });
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 60_000 });

  // Replace this illustrative flag with the application's actual ready signal.
  await page.waitForFunction(() => window.webglSceneReady === true, {
    timeout: 30_000,
  });

  await page.screenshot({ path: 'webgl-page.png', type: 'png' });
} finally {
  await browser.close();
}

Run it with node screenshot-webgl.mjs. Puppeteer documents Page.screenshot() as its page capture API. Its guide demonstrates navigation with waitUntil: 'networkidle2', but that only describes a navigation wait; it is not a universal WebGL readiness guarantee. [Puppeteer screenshot guide]

2. Choose the headless mode and GPU configuration

Puppeteer’s headless: true default launches the newer Chrome headless mode. Setting headless: 'shell' selects the separate chrome-headless-shell binary. The shell can be more performant for automation that does not need the full Chrome feature set, but it does not completely match regular Chrome. Compare modes for your page instead of assuming one is best for every WebGL workload. [Puppeteer headless modes]

Mode When to use it Configuration
Default headless First choice for a normal Puppeteer capture. puppeteer.launch() or headless: true
Headless shell When you specifically want the separate shell binary and have checked its rendering against your page. headless: 'shell'; add --enable-gpu for GPU acceleration.
Headful Chrome A comparison run when your environment supports a display and headless output differs. headless: false
const browser = await puppeteer.launch({
  headless: 'shell',
  args: ['--enable-gpu'],
});

Puppeteer’s troubleshooting guide specifies --enable-gpu for GPU acceleration in headless shell. The flag does not create GPU access if the host lacks suitable drivers or graphics support. Chrome generally detects and enables GPU when appropriate drivers are available. Avoid piling on unrelated browser flags as a first fix for a blank canvas: first check the mode, runtime graphics support, render readiness, and context state. The same Puppeteer guide discusses --no-sandbox for specific Linux sandbox failures and strongly discourages running without the sandbox; it is unrelated to enabling WebGL screenshots. [Puppeteer troubleshooting]

3. Capture the whole page or only the WebGL canvas

Page.screenshot() captures the viewport by default. Use fullPage: true for the full document, or use a canvas element handle when you only need the rendered scene. Puppeteer’s element screenshot scrolls the element into view if necessary, then captures it through the page screenshot machinery. [Page.screenshot API, ElementHandle.screenshot API]

// Full document
await page.screenshot({ path: 'full-page.png', fullPage: true, type: 'png' });

// A specific canvas (use an app-specific selector when there are several)
const canvas = await page.waitForSelector('#scene canvas', { timeout: 15_000 });
if (!canvas) throw new Error('WebGL canvas was not found');
await canvas.screenshot({ path: 'webgl-canvas.png', type: 'png' });

The screenshot options also include clip, captureBeyondViewport, and background handling. PNG is the documented default type; specify type: 'jpeg' or type: 'webp' when you need those formats. For a clipped capture, set a deliberate rectangle and check that it still covers the canvas at the chosen viewport. Responsive layouts, CSS scaling, and multiple canvases can change which pixels are included, so inspect the output dimensions and crop.

4. Make the readiness signal meaningful

A page can finish network activity before its renderer has completed a frame. Prefer an application-owned signal that is set only after assets are loaded and the intended scene has rendered. A selector can work if the app adds it at the right time; a fixed delay is a fallback when the page provides no readiness hook, but it is less reliable and may either wait too long or capture too early.

// Example: wait for an app-owned DOM marker instead of an illustrative global.
await page.waitForSelector('[data-scene-state="ready"]', { timeout: 30_000 });

// If no signal exists, a delay is only a heuristic.
await new Promise(resolve => setTimeout(resolve, 1500));

If you own the WebGL app, expose a readiness marker after the scene has loaded and the app has completed the render work needed for the screenshot. Do not treat the sample window.webglSceneReady as a Puppeteer or browser API; it is merely an example contract between a page and its capture script.

5. Diagnose blank or stale WebGL captures

Symptom Likely cause What to check
Canvas is blank The app has not rendered yet, GPU support is unavailable, or the WebGL context is lost. Wait for an app readiness signal; verify mode and host graphics support; inspect the context.
Old scene or incomplete assets Navigation settled before the app finished loading or rendering. Use an app-specific ready marker or render-complete event rather than relying only on network idle.
Different image in shell and default mode The binaries behave differently or graphics support differs in the runtime. Compare default headless, shell with --enable-gpu, and headful where available.
Canvas not found or clipped Selector mismatch, multiple canvases, responsive sizing, or off-viewport geometry. Use app-specific markup and inspect canvas bounds and resulting image size.
Capture hangs or times out The page never satisfies the chosen readiness condition. Check that the marker is reachable on success, set a finite timeout, and report a useful error.

WebGL exposes isContextLost() to check whether a context is lost. The webglcontextlost event indicates that the browser detected loss of the drawing buffer associated with that context. Resource demand, switching graphics processors, a stalled GPU operation, or a driver update can contribute to context loss; an application may need to restore resources and redraw after restoration. [MDN: isContextLost(), MDN: webglcontextlost event]

// Diagnostic only: adapt this to the actual canvas/context used by the app.
const diagnostics = await page.evaluate(() => {
  const canvas = document.querySelector('#scene canvas');
  if (!canvas) return { canvasFound: false };

  // Use the same context type the application uses when possible.
  const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
  if (!gl) return { canvasFound: true, webglContextFound: false };

  return {
    canvasFound: true,
    webglContextFound: true,
    contextLost: gl.isContextLost(),
  };
});
console.log(diagnostics);

This check is only a point-in-time diagnostic. It cannot establish that the desired frame was rendered. Asking for a different context type from the one the app created may also produce misleading results, so use app instrumentation or a page-provided status when possible.

6. Reliability, performance, and cost considerations

  • Pin a compatible browser setup: Puppeteer works best with its bundled Chrome for Testing; compatibility with other browser versions is not guaranteed. Keep the Puppeteer package and browser build consistent across capture workers. [Puppeteer browser management]
  • Choose the wait that matches the app: network idle is useful for navigation but can be unsuitable for pages with persistent connections, and it still does not prove that the WebGL scene is ready. A precise application signal avoids arbitrary over-waiting.
  • Reuse browsers carefully: launching Chrome has setup cost, while a long-lived browser can serve multiple captures. Isolate pages and close them after use; close the browser in a finally block as in the example. If a page or GPU context becomes unhealthy, recycle the worker rather than trusting later captures.
  • Budget for graphics resources: GPU availability and driver behavior vary by host. Concurrent heavy scenes compete for graphics and memory resources; set concurrency based on observed stability in your environment.
  • Capture only what you need: a canvas screenshot can reduce output size and avoid unrelated page content. Full-page captures may include a much larger document and can have different layout behavior.
  • Estimate cost from your own infrastructure: Puppeteer itself is open source, but browser workers, CPU/RAM, GPU-capable hosts, storage, and operations have deployment costs. The cited documentation provides no universal performance or cost benchmark for WebGL screenshots.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. For pages its browser can render, make a single request to receive an image or PDF. Cookie banners are accepted and removed along with known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See the API documentation for options and setup.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/webgl-page"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/webgl-page',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

Use a valid API key and replace the target URL with the page you want. Check response headers such as X-Page-Verdict and X-Billed to see whether a result was clean and billed. Sign up for 1,000 free screenshots a month with no card.

8. FAQ

Does Puppeteer guarantee WebGL output in headless mode?

No. The result depends on the selected Chrome mode, available graphics support, page readiness, and the app’s context state. Validate the output in the environment where captures will run.

Is networkidle2 enough?

It can indicate a navigation quiet point, but it does not guarantee that the application rendered the intended WebGL frame. Wait for a page-specific signal.

Should I use --no-sandbox to fix a blank screenshot?

No. Puppeteer documents that flag in connection with particular sandbox failures, and discourages disabling the sandbox. It does not enable WebGL GPU acceleration.

Can I save just the canvas?

Yes. Find the correct canvas and call its element handle’s screenshot() method after the scene is ready.

References