ScreenshotNeo

BlogHow-to

Puppeteer Screenshot Shows a Broken Layout After Network Idle: Troubleshooting

Network idle does not guarantee a page is visually ready. Diagnose broken Puppeteer screenshots with explicit viewports, app-specific waits, and page checks.

By the ScreenshotNeo team4 October 20268 min read

networkidle means network activity has quieted; it does not guarantee that your application has rendered the content and layout you expect. Set the viewport before navigation, wait for a page-specific ready condition, inspect the DOM and failed resources, then capture. The right condition depends on the page: a visible content selector, a known application flag, or a specific change in the rendered content is more useful than adding an arbitrary delay.

Why a Puppeteer screenshot can look broken after network idle

Navigation waits describe browser or network events. They are not a universal signal that a single-page application has finished updating, a delayed component has appeared, or a visual transition has ended. Puppeteer’s screenshot guide shows navigation followed by capture using networkidle2, while its Page API provides separate waits for selectors and functions. Choose a wait that reflects the state you need to photograph. See the Puppeteer screenshot guide and Page API.

A broken layout can also be a viewport mismatch, a stylesheet or font that failed to load, an unexpected DOM state, or a selector that targets the wrong element. These are diagnostic possibilities, not a single established cause. Inspect what the page actually contains at capture time instead of assuming the network wait is responsible.

Use a page-specific readiness check

This runnable example sets the viewport before navigation, records page and request errors, waits for a meaningful selector, checks its dimensions, and then saves a screenshot. Replace the URL and selector with values for the page you are diagnosing. The selector should appear only when the content you need is ready.

const puppeteer = require('puppeteer');

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

    // Set layout-affecting settings before navigation.
    await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });

    page.on('console', message => {
      if (message.type() === 'error') console.error('console error:', message.text());
    });
    page.on('pageerror', error => console.error('page error:', error.message));
    page.on('requestfailed', request => {
      console.error('request failed:', request.url(), request.failure()?.errorText);
    });
    page.on('response', response => {
      if (response.status() >= 400) {
        console.error('HTTP error:', response.status(), response.url());
      }
    });

    const response = await page.goto('https://example.com', {
      waitUntil: 'networkidle2',
      timeout: 60000
    });
    console.log('navigation status:', response?.status());

    // Use a selector that indicates the required content is ready.
    await page.waitForSelector('[data-page-ready="true"]', {
      visible: true,
      timeout: 30000
    });

    const state = await page.$eval('[data-page-ready="true"]', element => {
      const rect = element.getBoundingClientRect();
      const style = getComputedStyle(element);
      return {
        text: element.textContent?.trim(),
        width: rect.width,
        height: rect.height,
        display: style.display,
        visibility: style.visibility
      };
    });
    console.log('ready element:', state);

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

Install Puppeteer in your project with npm install puppeteer. If the app does not expose a readiness selector, use a condition grounded in its actual behavior. For example, wait for a known loading indicator to disappear, or wait until the expected text is present. Do not copy the example selector without confirming that the page provides it.

Wait for a specific application state

When the app has a reliable global readiness flag or content condition, waitForFunction can poll it in the page context:

await page.waitForFunction(() => {
  return window.appReady === true &&
    document.querySelector('#results')?.children.length > 0;
}, { timeout: 30000 });

Replace window.appReady and #results with real conditions from the application. A condition that only checks for an element that exists in the initial shell may pass before the data or final layout is ready. If a transition changes the layout after content arrives, include a meaningful end-state check in the predicate where the page makes one available.

Troubleshooting sequence

  1. Make the failure reproducible. Record the Puppeteer version, browser channel or version, URL, viewport dimensions, device settings, and whether you are taking a full-page or element screenshot. Confirm that the same URL and settings reproduce the visual issue.
  2. Set and verify the viewport first. Call page.setViewport() before page.goto(). Compare its width and height with the layout you expect. Device scale affects pixel output; viewport width is especially relevant to responsive breakpoints. Puppeteer advises setting the viewport before navigation because some sites react unexpectedly to a phone-sized viewport change. Some changes involving isMobile or hasTouch can trigger a reload. See Page.setViewport and Viewport.
  3. Check navigation status and errors. Log the response from goto(), console errors, page errors, failed requests, and HTTP error responses. A completed navigation is not proof that the app populated correctly. Compare the run that looks right with the one that looks broken.
  4. Wait for the needed content. After navigation, wait for a visible selector or a page-specific function condition. Give it a bounded timeout so a missing condition fails clearly rather than silently producing an incomplete capture. Puppeteer documents waitForSelector and waitForFunction in the Page API.
  5. Inspect DOM state and dimensions. Use page.evaluate() or an element handle to verify expected text, computed styles, visibility, and bounding-box dimensions immediately before capture.
  6. Inspect the resources used for styling and content. Confirm the expected CSS, font, image, and data requests succeeded. If request interception is enabled, make sure every request is continued, responded to, aborted, or served from cache: intercepted requests stall until handled.
  7. Capture at the correct scope. Use page.screenshot() for the page, or locate the intended element and call its screenshot method. Confirm the element selector matches the component you want.

Choose the right wait and capture scope

Choice What it observes When it helps
load The page load event When the page’s load event is an appropriate navigation milestone.
networkidle0 / networkidle2 Network activity reaching the documented idle threshold When a network quiet period is useful, alongside an application readiness check.
waitForSelector A matching selector appearing, optionally visible When a particular element signals that the needed content is present.
waitForFunction A predicate becoming true in the page context When readiness depends on app state, content, or several DOM conditions.
page.screenshot() The page capture For a viewport or full-page screenshot.
elementHandle.screenshot() The selected element For a component capture; Puppeteer attempts to scroll a hidden element into view.

There is no universally correct navigation wait for every app. Use a navigation wait to establish that navigation reached a useful milestone, then verify the application-specific state. For API details and version-specific behavior, consult the Page API matching the Puppeteer version in your project.

Common broken-layout symptoms and fixes

Symptom What to check Practical fix
Wrong responsive arrangement Viewport width and height, device scale, and mobile or touch emulation Make those settings explicit before navigation; capture at the intended dimensions.
Shell appears but content is missing Whether the data or target content has appeared in the DOM Wait for a selector or application predicate that represents populated content.
Page is unstyled or partly styled Stylesheet and font responses, console errors, failed requests, request interception Resolve failed resources and ensure intercepted requests are handled.
Images are absent, especially lower down Whether images are lazy-loaded and whether the required content has been brought into view Inspect the image state and the page’s loading behavior before capture; do not assume network idle triggered lazy content.
Screenshot shows the wrong component Selector match, element visibility, and element dimensions Wait for and verify the intended element, then use its handle for an element screenshot.
Wait times out Whether the selector or predicate is valid for this route and whether the app reached an error state Inspect the DOM and page errors; correct the condition or fix the application failure before increasing the timeout.

Full-page and element screenshots

For a page capture, use page.screenshot({ fullPage: true }) when the whole document is needed. Full-page capture does not by itself prove that all lazy content has loaded. Check the page’s image and content state, and use the loading behavior appropriate to that site. For a component, wait for its selector, check its dimensions, and take an element screenshot:

const element = await page.waitForSelector('#report-card', {
  visible: true,
  timeout: 30000
});
if (!element) throw new Error('Report card was not found');
console.log('element box:', await element.boundingBox());
await element.screenshot({ path: 'report-card.png' });

Use Page.screenshot() and ElementHandle.screenshot() according to the desired scope. The latter attempts to scroll a hidden element into view. Read the screenshot guide for the supported capture options.

Performance, reliability, and cost

  • Prefer condition-based waits. A fixed sleep waits the same amount whether the page is ready quickly or slowly. A selector or predicate can proceed as soon as its defined condition is true, while still using a timeout to bound the wait.
  • Keep diagnostics useful. Log failed requests and page errors while troubleshooting; avoid retaining excessive logs in routine capture jobs.
  • Make retries conditional. If a capture fails, check whether the underlying condition was transient, such as a failed request, or deterministic, such as a wrong selector. Retrying cannot repair a wrong viewport or invalid readiness condition.
  • Account for browser work. Launching and managing a browser has setup and runtime costs in your own environment. Reuse and concurrency decisions depend on your workload and deployment; the cited Puppeteer documentation does not establish universal benchmark or cost figures.
  • Use bounded timeouts. A bounded navigation and readiness wait makes failure visible and prevents a capture job from waiting forever. Set limits according to the pages and environment you control.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF, and its options include viewport and device presets, full-page capture, selector-based element capture, wait conditions, custom CSS and JavaScript, request blocking, and more. See the ScreenshotNeo website and API documentation.

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}`);
  • Cookie and consent banners are accepted or removed, along with known newsletter popups and chat widgets, before the shot.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers say which page verdict was returned and whether it was billed.
  • An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
  • The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, with no card.

FAQ

Does network idle mean the page is ready to screenshot?

No. It describes network activity, not whether the application reached the visual state you need. Follow it with a meaningful selector or app-specific readiness check.

Should I always use a fixed delay after network idle?

No. A fixed delay may hide timing variation and waste time. Prefer an observable page condition; use a delay only when the page behavior gives you a specific reason to wait for elapsed time.

Why does my screenshot change when I enable mobile emulation?

Viewport and device settings can affect responsive layout, and some mobile or touch setting changes can trigger a reload. Set the intended viewport and device settings before navigation.

Can a full-page screenshot include images that load lazily?

Do not assume it will. Check whether the page has loaded the images and content you need before capturing; lazy-loading behavior depends on the page.

References