That 800x600 Default Will Bite You
Headless Chromium's default viewport is 800x600. I found this out after a screenshot pipeline I'd built for a client started producing layouts with collapsed sidebars and stacked navigation that looked nothing like the actual site. Took me longer than I'd like to admit to realize the problem wasn't CSS — it was the viewport nobody bothered to set.
Always set it explicitly.
// Puppeteer
await page.setViewport({ width: 1366, height: 768 });
// Playwright
const context = browser.newContext({
viewport: { width: 1920, height: 1080 }
});
Picking Viewport Sizes That Actually Matter
There's no magic list. Depends entirely on what you're building screenshots for. But here's what I've settled on after running automated screenshots across dozens of projects — five widths that cover the vast majority of real-world layouts without wasting time on obscure devices nobody uses:
1920x1080 — standard desktop monitor. Your designers probably built the site at this width first. 1366x768 — the most common laptop resolution worldwide, and the one teams forget to test. Half the visual bugs I've caught in screenshot diffs showed up at this width specifically. 768x1024 — tablet in portrait mode. 375x812 — roughly an iPhone 12/13/14. 1200x630 — only if you're generating og:image social previews, where the exact ratio matters.
Setting width to 375 doesn't simulate a phone. You're missing device pixel ratio and the user agent string. A real iPhone renders at 3x DPR, so if your site uses srcset or checks window.devicePixelRatio, your screenshots won't match what actual users see.
await page.setViewport({
width: 375,
height: 812,
deviceScaleFactor: 3
});
Playwright makes this easier with built-in device profiles:
const iPhone = devices['iPhone 13'];
const context = await browser.newContext({ ...iPhone });
Handles viewport, DPR, user agent, touch — all of it in one line.
Full-Page Screenshots and Lazy Loading
When you take a full-page screenshot, the viewport height you set basically gets thrown out. The browser captures the entire scrollable area. That's fine until you realize half your images are lazy-loaded and the browser never scrolled down to trigger them. You end up with placeholder boxes or blank spots in the bottom half of the capture.
// scroll down to trigger lazy images, then back up
async function scrollPage(page) {
await page.evaluate(async () => {
const distance = 300;
while (window.scrollY + window.innerHeight setTimeout(r, 150));
}
window.scrollTo(0, 0);
});
}
await scrollPage(page);
await page.screenshot({ fullPage: true });
Hacky? Yeah. But it works, and I haven't found a cleaner way to handle it reliably across different sites.
Test at Your Breakpoints, Not Generic Ones
Look at your actual CSS media queries. If your layout shifts at 1024px, screenshot at both 1024 and 1025. That one-pixel boundary is where things break — menus half-open, grid columns overlapping, buttons shoved off screen. Generic device lists from blog posts won't catch these. Your own breakpoints will.
For docs, marketing materials, or anything that'll be viewed on a HiDPI display, bump deviceScaleFactor to 2. A 1366x768 viewport produces a 2732x1536 image. Looks sharp. File sizes quadruple though, so think about storage if you're archiving thousands of screenshots over time.
await page.setViewport({
width: 1366,
height: 768,
deviceScaleFactor: 2
});
For PDF rendering, viewport math is different entirely — A4 at 96dpi works out to about 794x1123 pixels. Don't try to match a screen resolution for print output.