How to Screenshot a Web Page with WebGL in Playwright
Capture WebGL pages reliably in Playwright by waiting for an app-specific ready signal, controlling the render state, and diagnosing blank or inconsistent screenshots.
To screenshot a WebGL page in Playwright, navigate to it, wait until the application has drawn the scene you want, then call page.screenshot(). A page load event does not guarantee that a WebGL scene is ready: the app may still be compiling shaders, loading textures, or advancing its render loop. Playwright has no universal WebGL-ready event, so expose or identify a readiness condition specific to your app.
For a visual regression test, use Playwright Test’s toHaveScreenshot(). It waits for two consecutive screenshots to match before comparing the result with the expected snapshot. Keep the browser and host environment consistent when creating and checking baselines.
1. Capture a WebGL page with an app-specific ready signal
The simplest reliable pattern is a page-side signal that becomes true only after the application has prepared the target scene. For example, your app can set window.sceneReady = true after loading required assets and rendering the frame you intend to capture.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 1280, height: 800 },
});
await page.goto('https://example.com/interactive-scene');
// Example only: your application must set this after the target frame is ready.
await page.waitForFunction(() => window.sceneReady === true, {
timeout: 30_000,
});
await page.screenshot({
path: 'webgl.png',
fullPage: false,
});
await browser.close();
Replace the URL and window.sceneReady condition with signals your application actually provides. The timeout should fit the page’s normal startup time while still failing promptly if the scene never becomes ready. waitForFunction() polls a page-side predicate; its default polling uses requestAnimationFrame.
Expose readiness from the application
A ready signal is useful only if it means what the test needs. Set it after the scene is initialized, required assets are available, and the desired state has been rendered. If your app has a renderer or scene manager, set the flag from the same code path that knows those steps have completed.
// Application-side sketch. Adapt this to your renderer and loading flow.
async function prepareCaptureState() {
await loadSceneAssets();
setSceneToCaptureState();
await renderTargetFrame();
window.sceneReady = true;
}
If you cannot change the application, wait for a meaningful observable condition, such as a status element becoming visible or a loading marker disappearing. A selector is only a valid readiness signal if the application updates it after the canvas has rendered the intended frame.
2. Choose the right screenshot scope
Playwright’s page.screenshot() captures the page. For a WebGL page, the screenshot includes the canvas as it is rendered in the browser. Choose the capture scope based on what the test or artifact needs:
| Need | Approach | Notes |
|---|---|---|
| Visible page at a fixed viewport | page.screenshot() |
Set a consistent viewport when creating the page. |
| Whole page, including content below the viewport | page.screenshot({ fullPage: true }) |
Full-page capture changes page dimensions. WebGL canvases and fixed-position elements may not behave like ordinary document content during this capture; verify the result for your page. |
| Only the canvas or another element | page.locator('canvas').screenshot() |
Useful for isolating the rendered scene. Use a selector that identifies the intended canvas. |
| Visual regression assertion | Playwright Test’s toHaveScreenshot() |
It waits for two consecutive screenshots to match, then compares against a baseline. |
// Capture just the WebGL canvas after the app signals readiness.
await page.waitForFunction(() => window.sceneReady === true, {
timeout: 30_000,
});
await page.locator('canvas').screenshot({ path: 'scene.png' });
// Or capture the full page.
await page.screenshot({ path: 'page.png', fullPage: true });
3. Make the frame repeatable
WebGL content often changes continuously. A screenshot taken a fraction of a second later can show a different animation frame, camera position, or loading state. For repeatable results:
- Set the application to a known scene, camera, and animation time.
- Wait for the app’s signal that the target frame has been drawn.
- Use the same viewport, browser version, operating system image, and browser settings for baseline generation and comparison.
- Keep headless mode consistent between runs.
Playwright screenshot options can disable CSS animations, but that does not control an application’s WebGL render loop. If the canvas animation affects the expected image, pause or deterministically set that animation in the application itself.
Use Playwright Test for visual comparisons
For visual regression, Playwright Test’s screenshot assertion waits for two consecutive screenshots to match and saves the last screenshot for comparison with the expected snapshot. This helps avoid comparing a frame while pixels are still changing, but it cannot make a continuously animated scene deterministic. Control the app state first.
import { test, expect } from '@playwright/test';
test('renders the target WebGL scene', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('https://example.com/interactive-scene');
await page.waitForFunction(() => window.sceneReady === true, {
timeout: 30_000,
});
await expect(page).toHaveScreenshot('webgl-scene.png');
});
Generate and compare snapshots in the same rendering environment. Operating system, browser version, settings, hardware, power source, and headless mode can affect the rendered output. See Playwright’s visual comparisons guide.
4. Run the capture in Chromium headless mode
Playwright supports different Chromium headless choices. Its browser documentation describes the bundled headless shell and the chromium channel for the newer headless mode. If a WebGL scene is blank or differs in headless Chromium, first confirm the mode and host configuration rather than assuming that one launch flag applies everywhere.
import { chromium } from 'playwright';
// Default Playwright Chromium launch.
const browser = await chromium.launch();
// To try Chromium's new headless mode, use the installed Chrome channel.
// Ensure Chrome is available in the environment running this script.
const chromeBrowser = await chromium.launch({ channel: 'chromium' });
Chromium’s headless GPU guidance documents --enable-gpu as a way to disable forced software rendering in headless Chrome. Treat it as a Chromium-specific diagnostic and verify its effect on the exact CI host and browser version you use. It is not a universal fix for every Playwright browser or environment.
const browser = await chromium.launch({
args: ['--enable-gpu'],
});
For background and current details, consult Playwright’s browser documentation and Chromium’s headless GPU guidance.
5. Troubleshoot blank or inconsistent WebGL screenshots
| Symptom | Likely cause | What to do |
|---|---|---|
| Blank canvas or fallback screen | The app has not completed setup, the target frame has not rendered, or the browser/host does not render the scene as expected. | Check the app’s readiness condition and inspect the page’s actual browser mode and host. Confirm that the scene renders in that environment before changing screenshot code. |
| Screenshot is taken before the scene appears | Navigation completion was mistaken for WebGL scene readiness. | Wait for an app-owned ready flag or a meaningful visual/status condition. Do not treat a fixed delay or networkidle as proof that the scene is rendered. |
| CI screenshot differs from local | Browser version, operating system, settings, hardware, power source, or headless mode differs. | Align the baseline and comparison environments, including the Playwright/browser version and headless mode. |
| Headless Chromium renders differently | The selected Chromium headless mode or GPU/software rendering path differs from the expected environment. | Check which headless mode is in use. On Chromium, test the documented GPU option on the target host and keep the chosen configuration consistent. |
| Visual assertion keeps failing on an animated canvas | The scene changes between captures, so two screenshots may not stabilize on the intended state. | Pause the app’s render loop or set a deterministic target frame before asserting. Disabling CSS animations does not stop WebGL animation code. |
| DOM inspection does not reveal scene details | Canvas pixels do not appear as ordinary accessible text or DOM content. | Use a screenshot as the visual reference. Playwright also documents screenshot-based interaction for canvas and WebGL interfaces in its vision mode guide. |
waitForFunction() times out |
The signal is absent, misspelled, set too early or too late, or never becomes true after an app failure. | Verify the condition in the page, set the flag only after the intended frame is ready, and inspect application errors. Keep a finite timeout so failed jobs do not hang. |
6. Performance, reliability, and cost
A Playwright screenshot adds browser work after the page has loaded; the total job also includes browser startup, navigation, app initialization, and any waiting for the scene. The research sources provide no supported benchmark for WebGL screenshot speed or reliability, so measure the complete flow on your own pages and CI host if runtime matters.
Reliability mostly comes from controlling the scene state and keeping the rendering environment stable. A screenshot can faithfully capture the current canvas while still being the wrong frame, so readiness and deterministic state matter more than adding arbitrary waiting time.
For cost, account for the compute and browser infrastructure used to run Playwright, especially when captures run in CI or at scale. There is no universal per-capture price in the Playwright documentation; it depends on where and how you run the browser.
7. Or skip the browser setup
If you need a screenshot of a public page without maintaining a browser capture job, ScreenshotNeo provides a website screenshot API and MCP server. It returns a PNG, JPEG, WebP, or PDF from one GET request. See the ScreenshotNeo API documentation for its options and response details.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com/interactive-scene \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://example.com/interactive-scene",
},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/interactive-scene',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
These examples request the same public page. ScreenshotNeo can remove cookie and consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are not billed; its MCP server lets AI agents take screenshots; and the Free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots. Its response includes page-verdict and billing headers. ScreenshotNeo is a screenshot service, so use Playwright when you need app-owned WebGL readiness signals, control of your browser environment, or deterministic control of the application’s render state.
Sign up free for 1,000 screenshots a month with no card.
8. FAQ
Does networkidle mean a WebGL scene is ready?
No. It describes network activity, not whether your application has rendered the intended WebGL frame. Wait for an application-specific condition.
Can Playwright screenshot only a WebGL canvas?
Yes. Use a locator for the canvas and call its screenshot() method after the scene is ready.
Will disabling animations freeze WebGL?
No. Screenshot animation options can affect CSS animations, but your app’s WebGL render loop needs to be controlled by the app.
Why can the same test produce different images on another machine?
Rendering can vary across operating systems, browsers, settings, hardware, power sources, and headless modes. Keep those conditions consistent for visual baselines.


