ScreenshotNeo

BlogHow-to

Puppeteer Screenshot of a Vue or Angular Page Before It Is Fully Rendered

Wait for the Vue or Angular content you need—not just navigation—before capturing a reliable Puppeteer screenshot.

By the ScreenshotNeo team4 October 20268 min read

To capture a Vue or Angular page after it has rendered the content you need, wait for a visible, content-specific selector or an application-owned readiness signal, then call page.screenshot(). Navigation finishing, a framework root appearing, or network activity becoming quiet does not by itself prove that the intended view is ready.

Here is a complete Puppeteer example. Replace the URL and [data-testid="page-ready"] with your page and a selector that appears only when the content for the screenshot is visible.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    page.setDefaultTimeout(15_000);

    await page.goto('https://example.com/dashboard', {
      waitUntil: 'domcontentloaded',
      timeout: 30_000,
    });

    await page.waitForSelector('[data-testid="page-ready"]', {
      visible: true,
      timeout: 15_000,
    });

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

The selector above is an example, not a Vue or Angular convention. If you own the application, add a stable readiness marker after the data and rendering steps relevant to the image have completed. Otherwise, wait for a stable element in the actual content. Avoid a generic root such as #app or app-root when it exists before async content loads.

1. Pick a readiness condition that matches the pixels

A useful wait condition describes the state you intend to capture. Prefer, in order:

  1. A visible content selector. Wait for a heading, result list, chart, or other element that demonstrates the relevant view is present. This is usually the clearest choice.
  2. An application-owned readiness marker. Set a marker only after all screenshot-relevant work—such as loading data and updating the view—has finished. Its meaning is an application contract, so keep it specific.
  3. A framework stability signal. This can help in a controlled test setup, but framework stability is not always equivalent to the exact view being visible.
  4. Network idle as a supplement. Use it only when the page’s network behavior makes idleness meaningful. Requests completing do not guarantee that later rendering work is done.

Puppeteer’s waitForSelector() can wait for a selector to appear or become visible. It resolves immediately if the selector already exists, so choose a selector whose presence means what you intend it to mean. Its documented default timeout is 30 seconds; set a deliberate timeout for your workflow and report failures clearly. Puppeteer: Page.waitForSelector()

2. Vue: wait beyond the mount point when needed

Vue’s mounted hook runs after that component’s DOM tree has been created and inserted. It does not guarantee that async components or components inside <Suspense> trees are finished. Nor does mounting prove that a network-backed view has loaded the data you want. Vue: mounted

For synchronous structure, an app-owned marker can be set after mounting. For a data-driven view, set it only after the relevant request succeeds and the view has updated. For example, the application could render data-testid="page-ready" conditionally once its own loading state is false and its required data is available. Puppeteer can then wait for that visible marker.

This distinction matters especially for client-rendered single-page apps: the browser may initially receive mostly empty HTML and need JavaScript execution before the view appears. Navigation lifecycle completion can therefore come before the page’s useful content. Vue: Server-Side Rendering

3. Angular: use Testability carefully

Angular’s Testability API exposes isStable() and whenStable(). In a controlled setup where Testability is available, you can ask the application to signal when it becomes stable. Angular documents that whenStable() invokes its callback when the app is stable or the supplied timeout expires, whichever comes first. Angular: Testability

Testability is available by default for applications bootstrapped through an NgModule. Applications bootstrapped with bootstrapApplication need the testing support providers for it to be available. Verify the setup for your app before relying on the API.

Stability can also take longer than the visible view: pending HTTP requests, timers, requestAnimationFrame, and other pending work may keep the app unstable. A persistent timer may prevent stability even when the target content looks ready. For screenshots, pair stability with a content-specific condition or prefer an app-owned readiness marker. Angular: Testability API

4. Use a browser-side predicate for multi-part readiness

When readiness depends on more than one signal, page.waitForFunction() lets you express a browser-side predicate. Keep it specific and bounded. This example waits for a visible result container and a completed loading indicator; adapt the selectors and state to the application.

await page.waitForFunction(() => {
  const results = document.querySelector('[data-testid="results"]');
  const loading = document.querySelector('[data-testid="loading"]');
  if (!results) return false;

  const visible = results.getBoundingClientRect().width > 0
    && results.getBoundingClientRect().height > 0;
  return visible && (!loading || loading.getAttribute('aria-busy') === 'false');
}, { timeout: 15_000 });

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

A predicate should encode the state that matters, not merely wait for elapsed time. If it times out, inspect the page and the predicate rather than silently taking a potentially blank screenshot.

5. Network idle, navigation waits, and fixed delays

Puppeteer’s navigation wait options include load, domcontentloaded, networkidle0, and networkidle2. The navigation default is load. The network-idle options describe periods with at most zero or two active connections; the documented idle period is 500 ms. These are browser lifecycle and connection conditions, not application-level render-complete guarantees. Puppeteer: Page.goto() · Puppeteer: lifecycle events

  • Use domcontentloaded plus a selector when the app fetches data after its initial document loads. The selector wait controls when to capture.
  • Consider network idle as an extra condition when the page has finite requests and idleness correlates with completed work. Long polling, analytics, or other persistent connections can prevent it.
  • Avoid fixed sleep as the main strategy. A delay wastes time on fast runs and can still be too short on slow ones. Use a fixed delay only for a known, bounded visual transition that has no observable completion signal.

Puppeteer’s documented default navigation timeout is 30 seconds. Choose timeouts that reflect your environment and fail with enough context to investigate; increasing a timeout does not fix a selector that never matches or an app that is stuck. Puppeteer: setDefaultNavigationTimeout()

6. Capture a whole page or a specific element

After readiness is established, capture the full page with page.screenshot(), or select the relevant element and capture just that element. Element screenshots can reduce irrelevant page content in a visual check.

// Whole page
await page.screenshot({ path: 'page.png', fullPage: true });

// A single element
const chart = await page.waitForSelector('[data-testid="chart"]', {
  visible: true,
  timeout: 15_000,
});
await chart.screenshot({ path: 'chart.png' });

Puppeteer documents screenshots on both Page and ElementHandle. Puppeteer: screenshots

7. Troubleshoot incomplete or unreliable captures

Symptom Likely cause Fix
Screenshot shows a blank app shell The wait targets a root container that exists before client rendering or data loading finishes. Wait for visible, screenshot-relevant content or an app-owned readiness marker set after that content is ready.
Selector wait succeeds immediately, but content is missing The selector was already present in a loading or empty state. Selector waits resolve immediately when it already exists. Use a selector unique to the completed state, or wait for a specific state change in waitForFunction().
waitForSelector times out The selector is incorrect, hidden, not rendered for this route, or the app failed to load the required view. Check the selector against the actual DOM, confirm the route and app state, and inspect browser errors and network failures. Use visible: true only when visible pixels are required.
networkidle0 never completes A long-polling connection, analytics request, or other continuing traffic prevents the idle condition. Wait for content directly; use network idle only if its connection semantics fit the page.
Angular stability wait never completes Pending timers, HTTP requests, animation frames, or other tasks keep the app unstable; Testability may also be unavailable due to bootstrap configuration. Verify Testability setup, identify the pending work, and use a content marker if unrelated background work does not affect the screenshot.
Screenshot catches a transition or partial update The app’s readiness signal fires before a visual transition or later update completes. Move the app-owned signal to the true desired state. If the transition is intentional, wait for its specific completion condition.
Works locally, fails intermittently in automation Rendering and data timing vary across runs, but the wait is based on a short fixed delay or an early lifecycle event. Use a semantic condition with a bounded timeout; log the route and failed condition so intermittent failures can be diagnosed.

8. Performance, reliability, and cost

A selector or app-owned marker usually lets fast runs proceed as soon as the relevant content exists, while a fixed delay holds every run for the full delay. Network idle may add waiting time or hang on pages with ongoing traffic. Keep navigation and readiness timeouts bounded, and treat a timeout as a failed capture rather than silently accepting an incomplete image.

For repeatable captures, keep the readiness condition stable across app changes, use the same viewport and route state, and distinguish page-load failure from readiness timeout in logs. A broader selector can be less brittle but may match too early; a highly internal selector can break during refactors. An explicit app-owned marker gives the clearest contract when you control the app.

Self-hosted Puppeteer has no per-screenshot API charge described here, but it does require browser setup and compute resources. Runtime and infrastructure costs depend on your own environment. For a managed screenshot API alternative, ScreenshotNeo bills only clean shots; bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing outcome.

Or skip the browser setup

ScreenshotNeo takes a screenshot or PDF from one GET request. Its API supports PNG, JPEG, or WebP output, and PDF capture. The ScreenshotNeo API documentation lists its parameters. Example with cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/dashboard -o shot.webp

Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/dashboard"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/dashboard',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie banners, popups, and chat widgets 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.

FAQ

Should I wait for Vue’s mounted hook?

It can indicate that a component’s own DOM has been inserted, but it does not cover async components, descendants in <Suspense>, or later data loading. Wait for the content your image needs.

Is Angular stable the same as visually complete?

No. Stability means the tracked app work is stable under Angular’s Testability model. It does not assert that a particular element is visible or that it matches your screenshot’s intended state.

Can I use networkidle2 for every SPA?

No. It is a connection-activity heuristic. Persistent requests can prevent it, and a page can still schedule relevant rendering after network activity quiets.

How long should the readiness timeout be?

Set a bound that accommodates normal app and environment variation while still surfacing stuck or broken runs. The documented selector default is 30 seconds, but a workflow-specific timeout and useful failure diagnostics are usually easier to operate.