WebGL in headless Chrome is the ability of a browser with no window to create a WebGL context and draw 3D graphics into a canvas, which depends on whether that browser can reach a GPU or falls back to SwiftShader, Chrome's software (CPU) implementation of the GPU.
Maps, 3D product viewers, charts built on WebGL and many landing-page hero effects need it. When it is missing, the page does not crash: it draws an empty canvas or a "your browser does not support WebGL" message, and your screenshot quietly shows that. This entry is part of the screenshot glossary.
What it means in plain terms
A WebGL page asks the browser for a drawing context with canvas.getContext('webgl') or 'webgl2'. Chrome passes those calls through ANGLE, a translation layer, to a real graphics API. There are three possible backends:
- A hardware GPU (Metal on macOS, Vulkan or OpenGL on Linux, Direct3D on Windows).
- SwiftShader, which runs the same work on the CPU. It is slower, but its output is stable across machines.
- Nothing.
getContextreturnsnull.
Servers usually have no GPU, so a headless capture on Linux depends on option 2. Chrome no longer falls back to SwiftShader for WebGL on its own in every configuration; the --enable-unsafe-swiftshader flag opts in.
flowchart TD A["canvas.getContext('webgl')"] --> B{GPU process can use a GPU?} B -- yes --> G["ANGLE → hardware GPU"] B -- no --> C{SwiftShader allowed?<br/>headless shell, or --enable-unsafe-swiftshader} C -- yes --> S["ANGLE → SwiftShader on the CPU"] C -- no --> N["null: no WebGL, empty canvas"]
Measured: the renderer each setup gives you
We asked each configuration for a WebGL context and read the unmasked renderer string through the WEBGL_debug_renderer_info extension.
| Launch | WebGL / WebGL 2 renderer |
|---|---|
| Puppeteer, new headless (default) | ANGLE Metal, Apple M1 Pro (hardware GPU) |
Puppeteer, headless: 'shell' |
ANGLE Vulkan, SwiftShader |
Puppeteer, headless: 'shell' + --disable-gpu |
ANGLE Vulkan, SwiftShader |
Puppeteer, new headless + --disable-gpu |
none (getContext returned null) |
New headless + --disable-gpu --enable-unsafe-swiftshader |
ANGLE Vulkan, SwiftShader |
New headless + --use-angle=swiftshader --enable-unsafe-swiftshader |
ANGLE Vulkan, SwiftShader |
Playwright chromium.launch() (headless shell) |
ANGLE Vulkan, SwiftShader (also with --disable-gpu) |
Playwright channel: 'chromium' (new headless) |
ANGLE Metal, Apple M1 Pro |
Method: 2026-10-15, Apple M1 Pro on macOS 27.0.1, Node 22.18.0, [email protected] with Chrome for Testing 155.0.8059.79, [email protected] with Chromium 156.0.8078.4. One probe per row; the renderer string is deterministic. A Linux server without a GPU would show SwiftShader or nothing in the hardware rows, so run the same probe on your own machines.
The row to remember is the fourth one. --disable-gpu appears in many copied Docker examples. With new headless mode, it removes WebGL completely.
Measured: what the capture actually contains
We drew one frame (a solid green clear colour) into a 400×200 WebGL canvas and read the centre pixel in two ways.
| Setup | WebGL context | page.screenshot() centre |
canvas.toDataURL() centre |
|---|---|---|---|
| New headless, default | yes | rgb(0, 153, 77), the scene | rgb(0, 0, 0), empty |
Same, preserveDrawingBuffer: true |
yes | rgb(0, 153, 77) | rgb(0, 153, 77) |
New headless, --disable-gpu |
no | rgb(255, 255, 255), page background | rgb(0, 0, 0), empty |
A browser screenshot reads the composited page, so it shows the scene. Reading the canvas from page code after the frame was shown returns cleared pixels unless the context was created with preserveDrawingBuffer: true. That is a WebGL rule, not a headless bug.
How to set it
Puppeteer
import puppeteer from 'puppeteer'; const url = process.argv[2] ?? 'https://example.com'; const browser = await puppeteer.launch({ // Software WebGL where no GPU exists. Only for pages you trust (see below). args: ['--enable-unsafe-swiftshader'], }); try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); await page.goto(url, { waitUntil: 'load' }); const renderer = await page.evaluate(() => { const gl = document.createElement('canvas').getContext('webgl'); if (!gl) return null; const info = gl.getExtension('WEBGL_debug_renderer_info'); return info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER); }); if (!renderer) throw new Error('WebGL is not available in this browser'); console.log('WebGL renderer:', renderer); await page.screenshot({ path: 'webgl.png' }); } finally { await browser.close(); }
Playwright
import { chromium } from 'playwright'; const browser = await chromium.launch({ args: ['--enable-unsafe-swiftshader'] }); try { const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); await page.goto(process.argv[2] ?? 'https://example.com'); const hasWebGL = await page.evaluate(() => !!document.createElement('canvas').getContext('webgl2')); if (!hasWebGL) throw new Error('WebGL 2 is not available'); await page.screenshot({ path: 'webgl-pw.png' }); console.log('wrote webgl-pw.png'); } finally { await browser.close(); }
Tested on 2026-10-15 with the versions in the method note above.
RenderScreenshot
There is no GPU or WebGL parameter in the API. To learn what the renderer gives a WebGL page, send the probe itself as HTML input and read the renderer string in the image:
import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const probe = `<pre id="out" style="font:20px monospace;padding:20px"></pre><script> const gl = document.createElement('canvas').getContext('webgl'); const info = gl && gl.getExtension('WEBGL_debug_renderer_info'); document.getElementById('out').textContent = !gl ? 'no WebGL' : info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER); </script>`; const client = new Client(process.env.RS_API_KEY); await writeFile('probe.png', await client.take(TakeOptions.html(probe).width(900).height(120)));
For real 3D pages, give the scene time to draw: wait for an element the page adds after its first frame with .waitForSelector(), as described in the wait options. We have not published a probe result from the API, and this SDK snippet was not run for this page.
Common mistakes
- Copying
--disable-gpuinto every launch. In new headless mode it turns WebGL off. - Testing on a laptop, deploying to a GPU-less server. The laptop uses its GPU; the server may get nothing. Run the probe where you capture.
- Reading the canvas instead of the page. Use
page.screenshot(), or create the context withpreserveDrawingBuffer: true. - Capturing at
load. Many WebGL scenes load textures and draw later. Wait for a ready signal. - Opening arbitrary URLs with the unsafe SwiftShader flag. Keep it for your own pages.
Related terms
For a full walkthrough with map tiles, read screenshots of maps and WebGL pages. To try the probe against our renderer, create a free account.