Puppeteer Full-Page Screenshots with Lazy-Loaded SVGs
Make lazy-loaded SVGs appear in full-page Puppeteer screenshots by triggering the page’s loader and waiting for a real readiness signal.
Direct answer: fullPage: true makes Puppeteer capture the page’s full height; it does not trigger lazy loading or prove that SVGs have rendered. Navigate, trigger viewport-based loading by scrolling when the page requires it, wait for a condition tied to the actual SVG implementation, then capture.
There is no universal “SVG is ready” check. First determine whether the target uses inline <svg>, an external SVG in an <img> or <object>, or JavaScript that inserts or updates SVG markup. Choose a readiness condition for that implementation.
1. Install Puppeteer and save a full-page screenshot
In a new project, install Puppeteer:
npm install puppeteer
Save this as capture.js. Set TARGET_URL to a page you are authorized to capture. Replace the example readiness selector and condition with ones that match that page.
const puppeteer = require('puppeteer');
const url = process.env.TARGET_URL || 'https://example.com';
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60_000 });
// Needed only when the page's lazy loader is triggered by viewport entry.
await page.evaluate(async () => {
const step = Math.max(200, Math.floor(window.innerHeight * 0.8));
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
window.scrollTo(0, y);
await pause(100); // Illustrative; tune to the target page's loader.
}
window.scrollTo(0, 0);
});
// Example only: wait for the page-specific SVG state you verified in its DOM.
await page.waitForFunction(() => {
const svg = document.querySelector('svg.chart.is-ready');
return svg && svg.querySelector('path');
}, { timeout: 15_000 });
await page.screenshot({ path: 'capture.png', fullPage: true });
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
Run it with TARGET_URL=https://your-site.example node capture.js. The selector svg.chart.is-ready, required child path, scroll step, and pause are examples, not universal or tested values. If the page has multiple graphics, wait for the expected set or a page-owned “rendered” state rather than just one SVG.
2. Identify how the SVG loads
Inspect the target page’s markup and scripts, or use Puppeteer to inspect its DOM. The right check depends on the implementation:
| SVG form | What to wait for | Common pitfall |
|---|---|---|
Inline <svg> |
The expected SVG exists and contains the expected rendered elements, or the application sets a ready state. | An empty SVG shell can exist before its paths or data arrive. |
External SVG in <img src="…svg"> |
The image element is complete and has nonzero natural dimensions; also check for a failed load. | complete can be true after a failed request, so check dimensions too. |
External SVG in <object> or <iframe> |
The element’s load event and the embedded content’s expected state, when accessible. | Cross-origin restrictions may prevent inspecting embedded DOM. |
| SVG inserted or updated by JavaScript | A page-specific DOM change, component state, or application signal after insertion and rendering. | Waiting for the outer container alone may catch an empty placeholder. |
For external image elements, a possible condition is:
await page.waitForFunction(() => {
const img = document.querySelector('img[src$=".svg"]');
return img && img.complete && img.naturalWidth > 0;
}, { timeout: 15_000 });
This only fits pages that use matching <img> elements. Adapt the selector to the real markup and check every expected image if there is more than one. Do not use img.complete as a readiness test for inline SVG.
3. Trigger lazy loading before capture
Lazy loading commonly defers resources until they approach the viewport; scrolling is one way pages trigger that work. A navigation event or network quiet may happen while below-the-fold assets have not yet been requested. Scroll through the relevant content, then wait for the particular SVGs or components you need.
The example scroll loop uses the document height at the start of the loop. Infinite-scroll pages can grow while scrolling, so a fixed initial height may miss newly appended content. For those pages, use a stopping condition based on the expected content, a known item count, or a page-owned completion signal. Avoid an unbounded loop.
Scrolling can also activate sticky elements, animations, and additional page behavior. Inspect the captured result and adjust the scroll strategy if those effects change the page. Scrolling alone does not guarantee that a custom loader has completed.
4. Understand wait conditions
Puppeteer’s navigation guide demonstrates waitUntil: 'networkidle2', and its API provides network-idle waiting. These are useful initial conditions, but network quiet means the network has been idle under Puppeteer’s criteria; it does not mean every graphic has been requested, decoded, or drawn. A fixed sleep is also only a time delay, not evidence that the SVG is ready.
- Use navigation waiting to establish that the page has reached an appropriate initial state.
- Trigger the loader if it depends on entering the viewport or another page event.
- Wait for a meaningful signal such as loaded image dimensions, expected SVG children, a component state, or known content count.
- Set timeouts and report which condition failed, so a missing asset is diagnosable rather than silently captured.
MDN notes that lazy-loaded media can still be unloaded when the window’s load event fires. That is why load, network idle, and arbitrary delays should not be treated as universal render-complete signals.
5. cURL, Python, and Node.js alternatives
cURL and Python do not run Puppeteer or control a browser. They are useful for checking whether an SVG URL is reachable, but they do not reproduce browser execution, viewport-triggered loading, or a full-page screenshot. Use the Node.js Puppeteer workflow above when the goal is specifically a browser-rendered capture.
For a quick HTTP check of a known external SVG URL:
curl -L --fail --output graphic.svg 'https://example.com/assets/graphic.svg'
python -m pip install requests
import requests
response = requests.get('https://example.com/assets/graphic.svg', timeout=30)
response.raise_for_status()
with open('graphic.svg', 'wb') as output:
output.write(response.content)
These checks verify an HTTP response, not that a page’s inline SVG or JavaScript-rendered component is ready in a browser.
6. Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Bottom-of-page SVGs are missing | The page never triggered viewport-based loading before capture. | Scroll through the relevant region and wait for the expected SVG or component state. |
| Network idle succeeds, but graphics are absent | Those resources were not requested yet, or the app renders after network activity stops. | Trigger loading and wait for an observable page-specific readiness condition. |
| A wait selector succeeds but the graphic is blank | The selector matched an empty wrapper or SVG shell. | Wait for expected paths, dimensions, data, or the application’s completed state. |
| External SVG check times out | The selector is wrong, the request failed, or the page uses inline SVG instead. | Inspect the DOM and browser console; choose a condition for the actual SVG form. |
| Capture is clipped or unexpectedly tall | The page changes size during lazy loading or has an expanding/infinite list. | Finish loading first, verify final document height, then capture; define a bounded endpoint for infinite content. |
| Navigation times out on a page that appears usable | Long-lived requests can keep a strict load condition from completing. | Choose a suitable navigation condition, then wait for the content you need with a separate bounded condition. |
| Screenshot differs from a normal visit | Scrolling activated sticky UI, animation, or other page behavior. | Inspect the page’s behavior, consider reducing motion with page-specific CSS, and verify the final viewport and scroll position. |
7. Performance, reliability, and cost
Full-page screenshots use more time and memory as page height and rendered content grow. Scrolling, per-step waits, and readiness checks add capture time; tune them to the actual loader rather than increasing sleeps without evidence. Limit concurrency if multiple large pages compete for browser resources, and close each browser when finished.
For reliability, make waits bounded, log the URL and failed readiness condition, and inspect output dimensions or a sample image in automated pipelines. Consider retrying transient navigation or resource failures, but do not retry a deterministic selector mismatch indefinitely. A screenshot can be produced successfully while still missing the content, so successful file creation alone is not a completeness check.
Running Puppeteer requires maintaining a compatible Node.js and browser environment and the compute to render each page. There is no universal cost figure: it depends on where and how often you run captures and the resources your pages need.
8. Or skip the browser setup
ScreenshotNeo offers a website screenshot API and MCP server. Its API can return a screenshot in a single GET request; see the API documentation for the available options. ScreenshotNeo is useful when you want a managed capture call instead of running a browser yourself. Its documented features include full-page capture with lazy images loaded and custom waits; for a page-specific SVG loader, confirm the relevant capture options in the docs.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Learn about ScreenshotNeo, then sign up free for 1,000 screenshots a month with no card.
9. FAQ
Does fullPage: true scroll the page to load lazy content?
It sets the screenshot extent to the full page. It does not establish that the page’s lazy-loading code has run; trigger and verify that separately.
Can one selector detect every ready SVG?
No. Inline markup, external images, embedded documents, and JavaScript-rendered graphics need different checks. Match the condition to the target page.
Is a two-second delay enough?
Not reliably. A delay can expire before a loader is triggered or before rendering finishes. Prefer a bounded condition tied to the expected content.
Why use networkidle2 if it is not a completion guarantee?
It can be a useful initial navigation condition. Pair it with the trigger and readiness checks the page requires.
Sources
- Puppeteer: Screenshots
- Puppeteer: ScreenshotOptions
- Puppeteer: Page.waitForNetworkIdle()
- MDN: Lazy loading
- Puppeteer issue #3202 (historical report, not evidence of current SVG behavior)


