ScreenshotNeo

BlogHow-to

How to schedule website screenshots after JavaScript animations finish

Schedule a browser job, wait for the page’s meaningful ready state and finite animations, then capture. Here are runnable Playwright and Puppeteer patterns.

By the ScreenshotNeo team4 October 20269 min read

Short answer: Use a scheduler to start a browser job, then make that job wait for the page’s actual ready condition and any finite JavaScript or CSS animations before capturing. A recurring schedule controls when the job runs; browser-side synchronization controls when the screenshot is taken.

For a page whose desired state is the natural end of its current animations, collect the active animations with document.getAnimations() and await their finished promises. If you need a repeatable visual-regression image instead, Playwright can disable animations for the screenshot. These approaches produce different results: waiting captures the natural final frame, while disabling changes how animated content is rendered.

1. Separate the recurring schedule from page readiness

Your CI scheduler, task runner, or operating-system scheduler launches the job at the chosen interval. The job then launches a browser, navigates to the site, waits for the relevant content and animation state, captures the page, and saves or uploads the image. The browser APIs do not prescribe a scheduler; scheduling syntax, secrets, storage, retries, and alerts depend on your runtime.

  1. Choose a recurring trigger in the environment where the job runs.
  2. Keep the target URL and credentials in that environment’s configuration or secret store.
  3. In the browser job, wait for a page-specific readiness signal, such as the relevant element becoming visible.
  4. Wait for finite animations if the final animated state is what you need.
  5. Capture and persist the image; report navigation, timeout, and upload errors through the job system.

Do not treat animation completion as proof that data has loaded. A page can have no active animations while a fetch or application update is still pending. If the application exposes a ready marker or a known element, wait for it as well. Prefer that signal over a guessed delay.

2. Playwright: wait for active animations, then capture

Install Playwright and its Chromium browser in your project using the official Playwright installation guide. Save this as capture.mjs and run it with Node.js. It waits for a page marker, then waits for animations currently returned by the Web Animations API before writing a full-page PNG.

import { chromium } from 'playwright';

const url = process.env.TARGET_URL ?? 'https://example.com';
const readySelector = process.env.READY_SELECTOR ?? 'body';
const outputPath = process.env.OUTPUT_PATH ?? 'capture.png';
const timeoutMs = Number(process.env.TIMEOUT_MS ?? 30000);

const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
  page.setDefaultTimeout(timeoutMs);
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
  await page.locator(readySelector).waitFor({ state: 'visible' });

  await page.evaluate(async () => {
    const animations = document.getAnimations();
    await Promise.all(animations.map(animation => animation.finished));
  });

  await page.screenshot({ path: outputPath, fullPage: true });
  console.log(`Saved ${outputPath}`);
} finally {
  await browser.close();
}

Set TARGET_URL, READY_SELECTOR, OUTPUT_PATH, and optionally TIMEOUT_MS in the job environment. Use a selector that means the content you care about is ready; body is only a basic fallback. The animation wait collects a snapshot of the animations at the time the evaluation runs. A later-starting animation is not included, and cancellation or restarting can affect the wait. For pages with staggered or continually created animations, use an application-specific completion signal or a bounded polling strategy tailored to that page.

Wait for a particular region

If only one region matters, collect animations from that element and its descendants instead of the whole document. Subtree collection can include animations targeting descendant elements and pseudo-elements.

await page.locator('#report').waitFor({ state: 'visible' });
await page.locator('#report').evaluate(async element => {
  const animations = element.getAnimations({ subtree: true });
  await Promise.all(animations.map(animation => animation.finished));
});
await page.locator('#report').screenshot({ path: 'report.png' });

Disable animation for a stable screenshot

For visual regression or a scheduled baseline where the goal is a stable image rather than the naturally animated end state, use Playwright’s screenshot option:

await page.screenshot({
  path: 'stable.png',
  fullPage: true,
  animations: 'disabled'
});

Playwright documents that disabled mode fast-forwards finite animations to completion and cancels infinite animations for the capture, then resumes them afterward. This affects the captured rendering. Choose it when that deterministic treatment is desired, not when you need to observe an animation’s natural timing or intermediate state. See the Playwright Page screenshot options.

Use screenshot stability assertions in Playwright Test

When writing a Playwright Test visual assertion, toHaveScreenshot() waits until two consecutive screenshots are identical before comparing with the expected screenshot. It is useful for visual regression, but it is a test assertion feature and does not prove that every application activity or background task has stopped. See Playwright screenshot assertions.

3. Puppeteer: use the same browser-side animation wait

Puppeteer’s screenshot API captures the page; pair it with an explicit browser-side wait when animation completion matters. Install Puppeteer with the official Puppeteer installation guide. This runnable ES module follows the same readiness-then-animation sequence.

import puppeteer from 'puppeteer';

const url = process.env.TARGET_URL ?? 'https://example.com';
const readySelector = process.env.READY_SELECTOR ?? 'body';
const timeoutMs = Number(process.env.TIMEOUT_MS ?? 30000);

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  page.setDefaultTimeout(timeoutMs);
  await page.setViewport({ width: 1440, height: 1000 });
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
  await page.waitForSelector(readySelector, { visible: true });

  await page.evaluate(async () => {
    const animations = document.getAnimations();
    await Promise.all(animations.map(animation => animation.finished));
  });

  await page.screenshot({ path: 'capture.png', fullPage: true });
} finally {
  await browser.close();
}

For Puppeteer navigation and capture settings, consult its official Page.screenshot API. The animation wait above is the Web Animations API pattern, not a built-in Puppeteer animation-finished option.

4. Schedule the job and handle failures

Put the browser script in the repository or job image, install its browser dependencies in the execution environment, then configure that environment’s recurring trigger to invoke the script. Pass the URL and output location through environment variables. Configure the job system to retain the image and logs, retry transient infrastructure failures where appropriate, and alert on repeated failures. Exact scheduler configuration is environment-specific.

Make failures visible instead of silently producing stale output. Use a navigation and readiness timeout; make upload success part of the job’s success condition; and record the target, scheduled run time, and error message. Avoid logging secrets such as authorization headers or cookies.

5. Choose the right synchronization method

Need Method Trade-off
Capture the natural final state of current finite animations Await document.getAnimations() and each animation’s finished promise Animations that start later are outside the collected set; truly infinite animations never finish.
Capture only one animated component Use element.getAnimations({ subtree: true }) Still waits only for animations returned at collection time.
Get a stable visual-regression image Playwright screenshot with animations: 'disabled' Finite effects are fast-forwarded and infinite effects canceled during capture.
Wait for repeated screenshot stability in a test Playwright Test toHaveScreenshot() Specific to Playwright Test; identical screenshots do not establish that all app work is complete.

6. Handle edge cases

  • Infinite animations: A natural-finish wait can hang forever if an animation never finishes. Identify such animations and exclude them, use an app-owned ready signal, or use Playwright’s disabled screenshot mode when its rendering behavior suits the task.
  • Animations begin after the wait starts: The collected list is a point-in-time snapshot. Wait for the trigger or application state that starts the animation before collecting, or have the application signal when all relevant motion is done.
  • Animation canceled or replaced: A canceled animation’s finished promise may reject. Decide whether cancellation means the desired state has been reached; handle rejection accordingly rather than letting a scheduled job fail unexpectedly.
  • Reduced motion and media preferences: The site may use different motion behavior based on browser settings. Set the expected preference explicitly if the screenshot must represent a consistent audience or test configuration.
  • Lazy content and scrolling: Full-page capture does not itself prove every lazy-loaded item has appeared. Scroll or otherwise trigger the content the page requires, then wait for its readiness and animation state.
  • Content changes without motion: An animation wait cannot detect a pending data fetch or a delayed DOM update. Wait for a page-specific element, state, or event.
  • Several independent pages: Isolate browser pages and outputs per target, use bounded concurrency, and give each capture its own timeout so one stuck site does not stall the entire schedule.

7. Troubleshooting

Symptom Likely cause Fix
The job hangs during the animation wait An animation is infinite, or the page continually starts animations. Inspect the relevant element’s animations; exclude endless motion, wait for an app readiness signal, or use disabled animation capture if appropriate.
The image captures before the animation starts The animation was created after getAnimations() collected its snapshot. Wait for the trigger or state that starts it before collecting animations; use a page-specific completion signal for staggered effects.
The wait rejects and the scheduled run fails An animation may have been canceled or replaced while awaiting finished. Handle cancellation according to the desired page state, or synchronize on the application’s final-state marker.
The screenshot is stable but contains old or missing data Visual stability and animation completion do not establish application data readiness. Wait for a selector or explicit app state that confirms the required content has loaded.
Navigation or selector wait times out The site is slow, unavailable, redirected, or the selector is wrong or hidden. Check the URL and selector, inspect the job’s page logs, and set a realistic bounded timeout.
Full-page image omits lower-page content Content may be lazy-loaded only after scrolling or intersection. Trigger the required content before capture and wait for its ready state.
Capture succeeds but the scheduled artifact is missing The image save or upload step failed independently of browser capture. Verify the output path or upload response and make persistence failure fail the overall job.

8. Performance, reliability, and cost

Waiting only on the relevant region can avoid waiting for unrelated motion elsewhere on the page. A fixed sleep always adds its full delay and may still be too short; condition-based waits finish when their condition is satisfied, though slow or stuck pages still need a timeout. Browser startup, navigation, page scripts, rendering, and image storage all contribute to a run; the research sources provide no benchmark for these workflows, so measure the job in your own environment.

For reliability, use bounded timeouts at navigation, readiness, animation, and persistence steps. A single animation collection can miss later work, so coordinate with the app’s lifecycle when animations are created dynamically. Keep retries for transient job or network failures bounded, and avoid retrying a page-specific logic error without changing its conditions. Self-hosted browser automation costs depend on compute, execution frequency, storage, and operations; there is no universal cost figure.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Make one GET request after choosing the target URL; see the API documentation for options. This captures the page without setting up your own browser job:

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}`);
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, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed; response headers report the page verdict and billing status. Its MCP server lets AI agents use screenshot, page-info, and PDF-capture tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. ScreenshotNeo’s capture options include wait conditions such as a selector, delay, or network idle, alongside full-page capture and other settings. For exact parameters and behavior, use the docs linked above.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

FAQ

Does waiting for animations also wait for images and API requests?

No. It waits for the animations collected from the document or chosen element. Use separate readiness conditions for data and assets that matter to the screenshot.

Can I use a fixed delay?

You can, but it is a guess: short delays can capture too early, while long delays waste job time. Prefer a known application state or animation completion condition, with a timeout.

Should I disable animations or wait for them to finish?

Wait for them when you need their natural final state. Disable them when a deterministic snapshot is more important and Playwright’s documented fast-forward and cancellation behavior is acceptable.

Does a recurring schedule make the screenshot happen at an exact instant?

No. The schedule starts the job; startup, navigation, readiness, and capture take additional time. Record the actual capture time if it matters to downstream consumers.

References