đź“‹ Table of Contents
The Problem with Modern SPAs
If you have ever tried to take a screenshot of a modern React, Vue, or Next.js application using standard tools, you've likely seen it fail. You end up with a blank white screen, missing hero images, or a loading spinner.
This happens because server-rendered HTML is no longer the final state. The browser needs time to evaluate JavaScript bundles, fetch API payloads, mount components, download fonts, and trigger lazy-loaded images.
In this guide, we'll explore the technical solutions to these synchronization issues when using Puppeteer/Playwright, and how to simplify the process using a managed API.
Fixing Missing Images (Lazy Loading)
By default, many modern frameworks use native image lazy loading (loading="lazy") or
JavaScript-based Intersection Observers to defer loading images until they enter the viewport.
If you take a full-page screenshot without scrolling, those images will appear as blank white boxes. To fix this in Puppeteer, you must programmatically scroll down the page to trigger the image loads.
// Scrolling to the bottom in Puppeteer to trigger lazy loads
await page.evaluate(async () => {
await new Promise((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
Waiting for Web Fonts
If your screenshots look completely different from the actual website because the text is rendered in Times New Roman or Arial, the browser captured the screenshot before the external web fonts (like Google Fonts) finished downloading.
You can force Puppeteer to wait until the document font faces are fully loaded by evaluating a small snippet on the page:
// Wait until all fonts are successfully loaded
await page.evaluateHandle('document.fonts.ready');
Waiting for API Data (networkidle)
If you are capturing a dashboard that fetches data from an external API, you need to wait until those network requests are complete so you don't capture an empty table or a spinning loading icon.
In Puppeteer, you control the navigation lifecycle using the waitUntil parameter:
// Wait until there are no more than 2 network connections for at least 500 ms
await page.goto('https://my-react-app.com/dashboard', {
waitUntil: 'networkidle2'
});
// Or be stricter and wait until ALL network connections finish
await page.goto('https://my-react-app.com/dashboard', {
waitUntil: 'networkidle0'
});
The Easy Way: Using SnapAPI
Implementing reliable scrolls, managing network idle states, and waiting for CSS animations to finish across different websites is incredibly brittle and requires constant maintenance.
With SnapAPI, all this complexity is handled automatically. Our rendering engine automatically detects SPAs, waits for web fonts, simulates user scrolling to trigger lazy loads, and evaluates network idle states.
If you have an exceptionally slow React app, you can simply add a delay parameter to give the
SPA more time to settle.
curl "https://snapapi.alterra-solutions.com/v1/screenshot?\
url=https://my-react-app.com&\
delay=2000&\
full_page=true&\
api_key=YOUR_KEY" -o react-app.png
No headless browsers to manage, no scroll scripts to decipher—just pixel-perfect screenshots.
Screenshot Your SPA Today
Get 100 free requests per month to capture your most complex web applications.
Get Started Free →