How to Capture a Website Screenshot After JavaScript Has Loaded in Apify
Use Apify with Puppeteer or Playwright to wait for the page content you need, capture it, and save the screenshot reliably.
To capture a JavaScript-rendered page in Apify, open it in a browser, wait for the specific content you need to appear, then call page.screenshot() and save the returned image. A navigation or load event can be useful, but it does not guarantee that a client-side application has finished rendering the content you care about.
The examples below use illustrative selectors. Replace them with a stable element or condition on the target site, and choose an image destination that fits your Actor workflow.
1. Choose a readiness condition
A screenshot records the browser at a point in time. The key decision is what must be true before capture. Match the wait to the page and the content in the image.
| Wait strategy | Use it when | Watch out for |
|---|---|---|
| Navigation or load event | A click or navigation opens a new document and the page is ready at document load. | Late client-side rendering may happen after the load event. |
| Specific selector | A known element appears when the content you need is ready. | The selector can be wrong or never appear. Set a timeout and handle the failure. |
| Page condition | Readiness is represented by application state, such as a result count or a populated list. | Make the condition reflect what must be visible in the screenshot. |
| Fixed delay | No dependable selector or page condition is available. | It may waste time and still be too short on a slow run. There is no universal delay. |
Prefer a content-specific selector or condition when one is available. Waiting for every network request or script to finish is not a universal solution: some pages keep polling or loading indefinitely. Apify’s Puppeteer Scraper documentation warns that waiting for all activity can mean “It would never stop waiting.”
2. Capture and store an image in an Apify Actor
This JavaScript example follows Apify’s documented flow with launchPuppeteer() from Crawlee and the Apify SDK key-value store. It uses a selector as the application-ready signal, captures a full-page PNG buffer, and stores it with the image content type.
import { Actor } from 'apify';
import { launchPuppeteer } from 'crawlee';
await Actor.init();
let browser;
try {
browser = await launchPuppeteer();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'load' });
// Replace this with a selector that represents the content to capture.
await page.waitForSelector('[data-page-ready="true"]', {
timeout: 15000,
visible: true,
});
const screenshot = await page.screenshot({ fullPage: true });
await Actor.setValue('screenshot', screenshot, {
contentType: 'image/png',
});
} finally {
if (browser) await browser.close();
await Actor.exit();
}
The selector and timeout are examples, not guarantees for a particular website. Check the current Apify SDK, Crawlee, and Actor image documentation for setup details that match your project versions. The core sequence is navigate, wait for the required content, capture, store, and close the browser.
For a local file instead of the key-value store, save the screenshot to a path. PNG is the default; the Academy tutorial documents JPEG through the type option.
const screenshot = await page.screenshot({
path: 'screenshot.png',
fullPage: true,
});
3. Use Playwright when the Apify workflow uses Playwright
When an action triggers navigation, Apify’s Playwright tutorial shows waiting for the load state after the action. Add a selector or page condition if the required client-rendered content appears later.
await page.click('a.some-link');
await page.waitForLoadState('load');
// Add a content-specific wait when the rendered result arrives after load.
await page.locator('[data-page-ready="true"]').waitFor({
state: 'visible',
timeout: 15000,
});
await page.screenshot({ path: 'screenshot.png', fullPage: true });
If you know navigation will occur, arrange the wait so it cannot miss a fast navigation. A navigation that completes before the code starts waiting can create a race. Follow the matching Playwright and Apify documentation for the version in your project.
4. Adapt the capture to the page
Wait for the content, not merely the shell
A selector such as a page root may exist before its data arrives. Pick an element that appears only when the needed result is ready, or check a meaningful property such as a non-empty result list. If content updates in stages, wait for the stage required by the image.
Choose viewport or full-page capture
Use the normal screenshot mode to capture the current viewport. Set fullPage: true when the image should include the full document. Full-page screenshots can be larger and may not represent content that only loads after scrolling; if the site lazy-loads sections or images, scroll or otherwise trigger them before capture, then wait for the relevant content.
Pick an output format and destination
- PNG: a lossless default suitable when image fidelity matters. The cited Apify Academy tutorial uses PNG by default.
- JPEG: available through the screenshot
typeoption in the tutorial; useful when a smaller lossy image is acceptable. - Apify key-value store: store the returned buffer with an image content type, as in the Actor example.
- Local path: use the screenshot
pathoption in a local or otherwise writable environment.
Apify’s Puppeteer Scraper tutorial also describes utils.puppeteer.saveSnapshot() as an alternative that can save screenshot data and optionally page HTML. Use the API matching your actor and package versions.
Use a fixed delay only as a fallback
If the page exposes no reliable readiness signal, a bounded delay can provide a simple fallback. It cannot prove the page is ready, so make the timeout reasonable for the workflow and inspect whether the captured content is complete. Do not treat a delay that worked once as a guarantee across network and application variations.
5. Troubleshooting
| Symptom | Likely cause | What to change |
|---|---|---|
| Selector wait times out | The selector is incorrect, hidden, conditional, or absent on this URL. | Inspect the rendered page, verify the selector, and wait for the actual content signal. Handle timeout as a failed or incomplete capture. |
| Screenshot is blank or missing the results | The capture happened before client-side rendering completed, or the wait matched an early page shell. | Choose a later, content-specific selector or condition and capture only after it is satisfied. |
| Navigation wait appears to hang | The page keeps network activity open or the chosen readiness event is not appropriate. | Wait for a bounded navigation/load event where relevant, then wait for the target content. Avoid requiring every request to finish. |
| Navigation completes before the wait begins | The code starts waiting after the navigation event already fired. | Set up the navigation wait in coordination with the click or action that triggers it, following the matching Playwright pattern. |
| Screenshot is captured but lazy content is absent | The content or image loads only after it enters the viewport. | Scroll the relevant section into view or trigger the page’s loading behavior, then wait for its content before capture. |
| Image cannot be found in the key-value store | The Actor wrote to a different key or the store operation did not complete. | Use the exact key when retrieving it, await Actor.setValue(), and check the run’s storage and logs. |
| Local screenshot path fails | The directory may not exist or may not be writable in the run environment. | Use a writable path or store the image buffer in the Actor key-value store. |
6. Performance, reliability, and cost
- Wait narrowly: a specific selector or condition avoids waiting for unrelated requests and makes the screenshot’s readiness rule easier to reason about.
- Bound waits: set timeouts and decide what the workflow should do when content does not arrive. A timeout should not silently become a successful-looking image.
- Keep browser lifetimes controlled: close the browser in cleanup code so failures during navigation or capture do not leave browser resources open.
- Control output size: viewport capture is generally less work than capturing a very long page; choose full-page only when the whole document is needed. Consider JPEG when its image quality is sufficient.
- Make retries deliberate: retry transient navigation or infrastructure failures where appropriate, but do not retry a deterministic bad selector indefinitely. Record whether a capture failed its readiness condition.
- Account for execution resources: browser work and stored image bytes consume Actor execution and storage resources. The research sources do not establish a universal runtime, resource requirement, or cost for a given site; estimate with your page and configuration.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Make one request with the page URL to return a PNG, JPEG, WebP, or PDF. Its documented options include waiting for a selector, delay, or network idle, along with full-page capture and custom headers, cookies, user agent, and authorization when a page needs them. See the ScreenshotNeo API documentation.
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 Bun.write('shot.webp', res);
Cookie banners, newsletter popups, and chat widgets are removed before the shot, with each cleanup step configurable. Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed; response headers report the page verdict and billing status. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up free and get 1,000 screenshots a month with no card.
FAQ
Does a load event mean JavaScript has finished?
No. It marks a navigation readiness point, but an application may render or update the needed content later. Wait for the content that must appear in the screenshot.
Can Apify save the screenshot without writing a local file?
Yes. The documented Actor pattern stores the screenshot buffer in the key-value store with an image content type.
What should I do if the page never reaches the ready condition?
Let the bounded wait fail clearly, check whether the selector and page state are valid for that URL, and decide whether the run should retry or record an incomplete capture.


