Take a Screenshot After Lazy-Loaded Map Tiles Finish Loading with Playwright
Wait for a map-specific readiness signal before capturing with Playwright. Learn why network idle is not proof that every visible tile has rendered.
Use Playwright to wait for a condition that means your map’s requested view is ready, then call page.screenshot(). There is no universal Playwright event that means every lazy-loaded map tile is complete: the reliable signal depends on the map library and the application. Playwright’s networkidle state only means there were no network connections for at least 500 ms, and Playwright discourages using it as a testing readiness strategy. Playwright Page API.
The dependable sequence is: navigate, establish the exact map view to capture, wait for that page’s map-ready condition, and take the screenshot. A quiet network can be a useful clue, but it does not prove that the desired tiles have rendered.
1. Identify a map-ready signal
First check the map library and your application code for a documented event, state, or callback that indicates the current view has finished rendering. Use that signal if it represents the view you intend to capture. The signal may mean tiles have loaded, rendering has settled, or the application has completed its own loading state; those meanings are implementation-specific.
If the app does not expose a map event, add a stable test hook to the page. For example, have the application set a data attribute on the map container after it decides the current view is ready. The sample below expects data-map-ready="true". That attribute is an application contract, not a built-in Playwright or map-library API.
A generic selector can also work if it truly reflects readiness, such as a loading indicator disappearing after the map signals completion. A map container merely existing, or a tile image appearing, is usually too weak: it may appear before the visible view is fully drawn.
2. Runnable Playwright example
This Node.js example uses Playwright’s built-in Chromium launcher. Set MAP_URL to your page and adjust the viewport, map interaction, readiness selector, and screenshot path to match your app. The application must set the sample readiness attribute only when the desired view is ready.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
});
try {
await page.goto(process.env.MAP_URL || 'http://localhost:3000/map', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
// Establish the intended map view here, before waiting.
// Example: await page.getByRole('button', { name: 'Show target area' }).click();
// This is an app-defined signal, not a universal map event.
await page.locator('#map[data-map-ready="true"]').waitFor({
state: 'visible',
timeout: 30_000,
});
await page.screenshot({
path: 'map.png',
fullPage: false,
animations: 'disabled',
});
} finally {
await browser.close();
}
})();
Install Playwright with npm install playwright. Install the browser binary with npx playwright install chromium. The example captures the current viewport; use fullPage: true only if a full-page image is what you need. Full-page capture changes the captured page area and may not correspond to the same map view or tile set as the viewport capture.
Wait with a page predicate
If readiness is represented by a JavaScript value rather than a useful DOM selector, use page.waitForFunction(). It resolves when the page function returns a truthy value. Pass only a condition that the application actually maintains:
await page.waitForFunction(() => {
return window.myMapApp?.currentViewReady === true;
}, { timeout: 30_000 });
await page.screenshot({ path: 'map.png' });
window.myMapApp and currentViewReady are placeholders. Expose a testable state in your application or replace them with the actual documented signal. See the Playwright waitForFunction API.
When there is no single completion event
Some applications cannot provide a definitive “all visible tiles done” event. In that case, define a page-specific condition with an explicit meaning. One option is an app-maintained counter of outstanding tile requests for the current view, combined with a rendered-state check. Another is a loading indicator that remains visible until the application’s own rendering work is complete. Avoid treating an arbitrary quiet interval as proof: later work, retries, or rendering can occur after the interval.
If you observe network requests in a test, scope them to the relevant tile endpoint and current view, and account for failures and retries. A request ending does not necessarily mean its tile was accepted and painted; an empty request count alone may also be true before lazy loading has started. Tie the condition to the app’s view and visible result.
3. Set the view before waiting
- Choose the viewport and device scale before navigation or before the map initializes.
- Navigate to the page and wait for an appropriate document state, such as
domcontentloaded. - Apply the target center, zoom, bounds, filters, and any required map interaction.
- Allow the desired view to trigger its lazy loading. Use the map’s documented readiness state or a reliable app-specific observable condition.
- Capture only after that condition is true. If visual testing, assert meaningful page or image state rather than equating an idle network with a complete map.
Lazy-loaded resources may be requested in response to viewport visibility or map movement. Waiting before selecting the intended view can therefore wait on the wrong tiles—or on no tiles at all. The order above is application guidance inferred from lazy-loading behavior; Playwright does not prescribe a map tile lifecycle.
4. Why networkidle is not tile completion
You can pass waitUntil: 'networkidle' to navigation or wait for that load state, but it means only that there were no network connections for at least 500 ms. It does not assert that each desired map tile was requested, succeeded, decoded, or painted. Conversely, maps may keep connections active or make later requests, so network idle may not arrive promptly.
Playwright explicitly marks networkidle as discouraged for tests and recommends web assertions to assess readiness. Prefer a map or application signal. The official Playwright touch-events example demonstrates interacting with Google Maps and checking a screenshot; it does not define a universal tile-loaded event.
5. Capture options and practical choices
| Need | Choice | What to consider |
|---|---|---|
| Capture the visible map view | page.screenshot({ path: 'map.png' }) |
Usually the appropriate choice for a map canvas or viewport. |
| Capture the whole document | fullPage: true |
Use only when the page, rather than the current map viewport, is the desired artifact. |
| Choose output format | Use a .png, .jpeg, or .webp path |
PNG is lossless; JPEG and WebP can reduce file size, with format support and quality controlled by screenshot options. |
| Capture a map element | page.locator('#map').screenshot({ path: 'map.png' }) |
Useful when surrounding controls should be excluded. Ensure the element’s bounds represent the intended map region. |
| Match device pixels | Set deviceScaleFactor in the browser context |
A higher scale captures more pixels and can increase memory and output size. Keep it fixed for repeatable visual comparisons. |
| Reduce animation differences | animations: 'disabled' |
Does not replace waiting for map readiness; use it to reduce animation-related capture variation. |
Playwright screenshot options and formats are documented in the Page API. For screenshot assertions in Playwright Test, use a meaningful expected state; screenshot assertions are a test-runner feature described in PageAssertions.
6. Python and cURL alternatives
Playwright’s Python API can perform the same browser automation. This sample assumes the same app-defined selector and a local map app. Install the Python package and its browser with pip install playwright and playwright install chromium.
import asyncio
import os
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(
viewport={"width": 1440, "height": 1000},
device_scale_factor=1,
)
try:
await page.goto(
os.environ.get("MAP_URL", "http://localhost:3000/map"),
wait_until="domcontentloaded",
timeout=30_000,
)
# Set the intended map view here, before waiting.
await page.locator('#map[data-map-ready="true"]').wait_for(
state="visible", timeout=30_000
)
await page.screenshot(path="map.png", animations="disabled")
finally:
await browser.close()
asyncio.run(main())
cURL cannot drive a browser, observe client-side map state, or take a Playwright screenshot. It can fetch an already-generated image endpoint, if your application provides one, but that is a different workflow and does not ensure the browser-rendered lazy tiles have completed.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Wait times out on the ready selector | The app never sets the test hook, the selector is wrong, or readiness depends on a view change that never happened. | Inspect the page state, verify the map interaction and selector, and expose a readiness signal tied to the target view. |
| Screenshot has blank or missing tiles | The capture ran before lazy loading began, before the map reached its target view, or before rendered output was ready. | Set the desired view first and wait for the app’s completion/render condition, not just the map container. |
Wait never resolves with networkidle |
The map or other page resources keep making requests. | Replace network idle with an app-specific readiness condition. |
| Network idle resolves, but tiles are still missing | A 500 ms quiet period does not prove tile success or paint completion. | Assert the map’s view-ready state and, where useful, a visual or tile-specific condition maintained by the app. |
| Intermittent screenshot differences | View size, map state, animation, font/render timing, or tile responses vary between runs. | Fix viewport and device scale, wait on stable app state, disable animations where appropriate, and compare only after the same view is established. |
| Navigation times out before the map can be checked | The page is unreachable, the main resource failed, or the navigation timeout is too short for the environment. | Check the URL and server first. Choose a navigation wait condition suited to the page, then use a separate bounded wait for map readiness. |
| Headless run differs from local interactive browser | Different viewport, browser binary, device scale, authentication, or environment configuration. | Set those inputs explicitly and ensure the run has the same permissions and map data as the intended environment. |
8. Performance, reliability, and cost
Waiting for a specific app signal is usually more efficient than adding a large fixed sleep: it lets the capture proceed as soon as the condition is satisfied and gives a bounded timeout when it is not. Keep the wait timeout long enough for your environment, but finite so a broken readiness contract fails visibly. Playwright documents that waitForTimeout() is for debugging and that timer-based production tests are inherently flaky; use state-based waits instead.
For repeatable runs, set the viewport, scale, target map view, and authentication consistently. Treat tile errors as a distinct state if the application can expose them: a “ready” state should not silently mean “some tiles may have failed.” When rendering is genuinely asynchronous, make readiness describe the completed view, not simply the end of a network request. No general performance benchmark or completion time applies across map providers and environments.
Browser automation has setup and runtime costs: the browser must be installed and launched, and the map must render in that environment. cURL alone does not replace this for client-rendered maps. For recurring captures, weigh maintaining a browser environment and page-specific readiness hook against a screenshot service that handles the browser capture request for you.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request returns an image or PDF, and its parameter names also work with those used by other screenshot APIs. See the ScreenshotNeo site and API documentation. For a rendered map page, provide the public page URL and use the supported wait options as needed; a service capture does not replace an application-specific guarantee that a particular map view has settled.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 example URL with your map page. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Does Playwright have a built-in map-tile-loaded event?
No universal event is established by the Playwright documentation. Use the specific map library’s documented signal or expose a readiness condition in the page.
Can I wait for all tile image elements to load?
Only if that is a reliable representation of the visible map for your implementation. Maps may use canvases, reuse or remove tile elements, and load resources based on view changes, so verify what the condition means for the map you are capturing.
Should I use a screenshot assertion?
For visual regression tests, yes, once the application reaches its intended state. A screenshot assertion checks visual output; it does not create a universal tile lifecycle signal. Playwright notes that screenshot assertions work with its test runner.
Can I use this for a map behind authentication?
Yes, if the Playwright context has the required session and the readiness condition is available after sign-in. Keep credentials in your test environment rather than hard-coding them in the script.


