Headless runs use a software renderer, so they cannot see GPU problems (why). This script measures a page on the real GPU.
import { chromium } from 'playwright';
const url = process.argv[2];
const browser = await chromium.launch({
headless: false,
args: ['--use-angle=d3d11', '--ignore-gpu-blocklist', '--window-position=-2400,0'],
});
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
// 1. warm-up: is WebGL available at all in this run?
await page.goto('about:blank');
const ok = await page.evaluate(() => !!document.createElement('canvas').getContext('webgl2'));
if (!ok) { console.log('GPU not ready, rerun'); process.exit(2); }
// 2. log every context and every loss
await page.addInitScript(() => {
const orig = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function (type, ...rest) {
const ctx = orig.call(this, type, ...rest);
if (/webgl/.test(type)) {
console.log(`[gl] ${type} ${ctx ? 'ok' : 'NULL'}`);
ctx && this.addEventListener('webglcontextlost', () => console.log('[gl] LOST'));
}
return ctx;
};
});
page.on('console', (m) => m.text().startsWith('[gl]') && console.log(m.text()));
// 3. measure frames
await page.goto(url);
await page.waitForTimeout(5000);
const fps = await page.evaluate(() => new Promise((r) => {
let n = 0; const t0 = performance.now();
const f = () => (++n, performance.now() - t0 < 2000 ? requestAnimationFrame(f) : r(n / 2));
requestAnimationFrame(f);
}));
console.log({ url, fps });
await browser.close();--window-position=-2400,0 keeps the window off-screen while still using the real GPU. Run each page a few times: the first launch after a driver reset can fail on its own.