How to Capture a Full Screenshot of a Lazy-Loaded Indian Web Page with Playwright
Use Playwright to load scroll-triggered content before capturing a full-page screenshot, with runnable code, troubleshooting, and a one-call API option.
Short answer: Playwright captures the full document with await page.screenshot({ path: 'full-page.png', fullPage: true }). But that option only controls screenshot height. It does not guarantee that images or sections loaded on scroll are ready. Scroll through the page first, wait for the content that matters, and then capture.
The browser behavior is the same for an Indian website as for any other site. The language or country does not change Playwright’s screenshot option; the result depends on the site’s loading logic, browser, viewport, and page state. No particular Indian site is assumed or tested here.
1. Why fullPage alone can leave blank sections
A full-page screenshot covers the full scrollable document as if the page fit on a very tall screen. It solves the problem of capturing beyond the current viewport, but not the separate problem of making content load.
Browsers can defer offscreen images and iframes marked with loading="lazy" until they are near the viewport. Those resources may still be outstanding when the window’s load event fires. Sites may also use Intersection Observer to fetch or render content as it approaches the viewport. So a screenshot taken immediately after navigation can contain blank placeholders even with fullPage: true.
Sources: Playwright screenshot guide, Playwright Page API, MDN: Lazy loading, and MDN: Intersection Observer.
2. Runnable JavaScript example
This script opens a page, scrolls in viewport-sized steps so viewport-triggered content has a chance to load, waits briefly between steps, returns to the top, and saves a full-page PNG. Replace the URL and tune the limits and readiness checks for the site.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
const url = 'https://example.com';
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.locator('body').waitFor({ state: 'visible', timeout: 15000 });
let stableRounds = 0;
let previousHeight = 0;
for (let i = 0; i < 40; i++) {
const height = await page.evaluate(() => document.documentElement.scrollHeight);
if (height === previousHeight) stableRounds++;
else stableRounds = 0;
if (stableRounds >= 2) break;
previousHeight = height;
await page.evaluate(() => window.scrollBy(0, window.innerHeight));
await page.waitForTimeout(500);
}
// Give the last viewport a chance to trigger its content, then return to the top.
await page.waitForTimeout(500);
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(300);
await page.screenshot({ path: 'full-page.png', fullPage: true });
} finally {
await browser.close();
}
})();
Install Playwright with npm install playwright. If the project has not downloaded its browser yet, run npx playwright install chromium. The height-stability loop is only a practical starting point: a finite page can stop growing before delayed content arrives, while an infinite feed may keep growing until the loop limit. Prefer a page-specific completion condition where possible.
3. A more reliable scroll and readiness workflow
- Choose the real browser context. Use the browser engine, viewport, authentication, cookies, locale, and device settings needed for the rendering you want. Playwright supports Chromium, Firefox, and WebKit; rendering can differ between engines.
- Wait for essential initial content. Wait for a known heading, main container, or application-ready signal.
domcontentloadedmeans the document was parsed; it does not prove that deferred images or application data are ready. Aloadevent is not proof that all lazy resources have loaded either. - Scroll through the relevant content. Move by roughly one viewport at a time and pause for the site to react. Intersection observers can watch a scrollable ancestor rather than the window, so scrolling the window will not activate content inside every nested panel.
- Wait for evidence, not just time. If the page exposes a final item, loading indicator, or image state, wait for that condition. Fixed delays are simple but can be too short on a slow response and unnecessarily long on a fast one.
- Handle the final viewport. Some loaders need a last scroll and settle period. Return to the top if you want the screenshot to begin from the page’s normal initial state.
- Capture and inspect. Save the full-page image and check for blank placeholders, missing images, sticky overlays, incomplete feeds, and unexpected height.
For image-heavy pages, a useful page-specific check is to wait until relevant images have completed or failed, rather than waiting for every image indiscriminately. For example, after scrolling, inspect img elements that are expected to appear and check their complete and naturalWidth properties. A broken image can be complete with a zero natural width, so completion alone is not success.
const imageSummary = await page.locator('img').evaluateAll(images =>
images.map(img => ({
src: img.currentSrc || img.src,
complete: img.complete,
naturalWidth: img.naturalWidth
}))
);
console.log(imageSummary.filter(img => !img.complete || img.naturalWidth === 0));
This check reports image state at the time it runs; it does not force a site’s JavaScript loader to request content. Avoid using it as a requirement that every decorative or intentionally broken image must succeed.
4. Variations for different page behavior
Finite pages with delayed content
Use a known readiness signal such as the last content card becoming visible, or wait until the relevant number of cards is present. A stable document height is useful supporting evidence, not a universal completion signal: content can load without changing height, and a page can pause before adding more.
Infinite scrolling
Set a clear stopping rule: for example, the expected last item appears, a “no more results” marker appears, or the number of loaded items reaches a known cap. Put a maximum scroll count or elapsed-time limit around the loop. Without a stopping rule, an infinite feed can keep extending and create an enormous screenshot.
Nested scroll containers
If the content lives inside a panel with its own scrollbar, scroll that panel. For a page-specific selector such as .results-panel, a simple approach is to scroll the element itself and pause after each step:
const panel = page.locator('.results-panel');
await panel.evaluate(async element => {
for (let i = 0; i < 30; i++) {
const before = element.scrollTop;
element.scrollTop += element.clientHeight;
await new Promise(resolve => setTimeout(resolve, 400));
if (element.scrollTop === before) break;
}
});
Adjust the selector and stop condition. If a site observes a different scroll root or loads only after a specific interaction, reproduce that behavior instead.
Viewport and device rendering
Set the viewport before navigation when responsive layout matters. The screenshot represents the selected browser rendering and page state. A mobile viewport can change which elements appear, how content wraps, and which lazy resources approach the viewport.
5. Screenshot options and practical choices
| Need | Playwright choice | What to consider |
|---|---|---|
| Entire document | fullPage: true |
Captures the full scrollable page; it does not trigger or await every lazy loader. |
| Current viewport only | Omit fullPage or set it to false |
Useful when only the visible state is needed. |
| Image format | Use type: 'png' or type: 'jpeg' |
PNG is lossless; JPEG is lossy. Check the installed API version for supported options and defaults. |
| Output location | path: 'full-page.png' |
Without a path, the screenshot API returns image bytes instead of writing the file. |
| Quality | quality for JPEG |
Applies to JPEG screenshots; it is not a PNG quality control. |
| Transparent background | omitBackground: true |
Relevant to supported image output; page content and browser behavior still matter. |
| Page state | Wait, scroll, and assert before capture | There is no screenshot flag that can infer every site’s definition of “ready.” |
Consult the official Page API for the exact options supported by your installed Playwright version. Very tall pages can produce large images and use substantial memory; for unusually long documents, consider capturing sections or limiting the page to the content you need.
6. cURL, Python, and Node.js alternatives
Playwright’s browser automation API is primarily used through JavaScript/TypeScript, Python, .NET, and Java. The workflow above is JavaScript. For Python, the equivalent browser automation uses Playwright’s Python package:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1365, "height": 900})
page.goto("https://example.com", wait_until="domcontentloaded", timeout=60000)
page.locator("body").wait_for(state="visible", timeout=15000)
previous_height = 0
stable_rounds = 0
for _ in range(40):
height = page.evaluate("document.documentElement.scrollHeight")
stable_rounds = stable_rounds + 1 if height == previous_height else 0
if stable_rounds >= 2:
break
previous_height = height
page.evaluate("window.scrollBy(0, window.innerHeight)")
page.wait_for_timeout(500)
page.wait_for_timeout(500)
page.evaluate("window.scrollTo(0, 0)")
page.wait_for_timeout(300)
page.screenshot(path="full-page.png", full_page=True)
browser.close()
Install it with pip install playwright, then install the browser with playwright install chromium. Python’s full_page=True is the corresponding option. cURL does not run Playwright or scroll a browser page; it can call a screenshot service that performs capture remotely. See the one-call option below.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank sections in full-page image | Content loads only when scrolled into view, or application data has not arrived. | Scroll the relevant root, wait for a page-specific signal, then inspect the image. |
Images are blank despite waiting for load |
Lazy images can remain deferred after the window load event. | Scroll them near the viewport and check their loaded state after the page reacts. |
| Only the first part of an infinite feed appears | The capture happened before more items were requested, or a loop limit stopped early. | Wait for the next item after each scroll and stop at a known final condition or explicit cap. |
| Nested panel content is missing | The panel, rather than the window, is the observed scroll container. | Scroll the panel itself and verify its content or scroll position advances. |
| Navigation times out | The page may keep network connections open, or required content is slow. | Choose an appropriate navigation milestone, set a considered timeout, and separately wait for essential content. A timeout should not be ignored if the page is genuinely incomplete. |
| Screenshot has unexpected layout | Viewport, browser engine, cookies, login, locale, or responsive state differs from the intended context. | Set the intended viewport and context before navigating; reproduce required state explicitly. |
| Screenshot is too tall or capture consumes too much memory | An unbounded feed or very long document is included. | Use a finite stopping condition, capture only needed sections, or constrain the page state. |
| Sticky header covers content | The site keeps a fixed or sticky overlay visible during normal scrolling. | Decide whether the overlay is part of the desired result; use site-specific CSS or capture sections if appropriate. |
8. Performance, reliability, and cost
For a reliable capture, spend time waiting on the content that matters rather than adding a large fixed delay everywhere. Scroll in steps that give viewport observers a chance to run, keep an explicit limit for feeds, and reuse a browser process when taking many screenshots if your application architecture allows it. Very tall full-page images can increase memory use and output size.
Playwright itself is an open-source browser automation framework; the operational cost of a self-managed capture depends on the machine, browser runtime, storage, and any proxy or hosting you choose. This guide makes no performance or price benchmark. For repeatable output, record the browser engine, viewport, page state, and completion rule used for each capture.
9. Or skip the browser setup
ScreenshotNeo offers a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF. Its clean-shot flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents and MCP clients.
For available capture options and configuration, see the ScreenshotNeo documentation. This cURL request saves a WebP shot of the target URL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Replace the target URL with the page you want to capture. ScreenshotNeo also supports full-page capture with lazy images loaded. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. AI agents can take screenshots through its MCP server. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month, with no card required.
10. Frequently asked questions
Does Playwright’s fullPage option scroll the page for me?
It captures the full scrollable document. Do not rely on it to reproduce every site’s scroll-triggered loading behavior; scroll and wait before capture.
Is a fixed delay enough?
It can be a simple fallback, but it can also be too short or waste time. A page-specific signal such as a final item or completed image state is more informative.
Why does the same URL produce a different image?
Content, responsive layout, browser engine, viewport, authentication, cookies, and timing can all affect what is rendered at capture time.
Can I use this for an Indian-language website?
Yes. Playwright captures the rendered browser page. Set the intended locale and context when those affect the page, and verify that the chosen fonts and content loaded in the browser.
What should I do with an endless feed?
Define a finite endpoint or maximum number of items and capture only that bounded state.


