ScreenshotNeo

BlogHow-to

How to Capture a Lazy-Loaded WordPress Page with Playwright

Scroll a WordPress page to trigger lazy-loaded content, wait for evidence that it is ready, then capture it with Playwright’s full-page screenshot option.

By the ScreenshotNeo team4 October 20269 min read

To capture a lazy-loaded WordPress page reliably with Playwright, navigate to it, scroll through the document in steps to trigger viewport-based loading, wait for a page-specific readiness condition, and then take a full-page screenshot. fullPage: true includes the full scrollable page in the image; it does not by itself ensure that every offscreen image or other deferred resource was requested and rendered.

The examples below use Playwright for JavaScript. Adapt the URL, selectors, scrolling behavior, and readiness checks to the WordPress site you are capturing. WordPress themes and plugins can implement lazy loading in different ways.

1. Install Playwright

Create a project and install Playwright, then install its Chromium browser:

npm init -y
npm install playwright
npx playwright install chromium

Save the following as capture-wordpress.js. Run it with node capture-wordpress.js.

2. Scroll, verify readiness, and capture

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  try {
    const page = await browser.newPage({
      viewport: { width: 1280, height: 900 },
      deviceScaleFactor: 1
    });

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

    // Wait for a meaningful page element. Replace this selector with one
    // that identifies the content you need to capture.
    await page.locator('main').waitFor({ state: 'visible', timeout: 15_000 });

    // Scroll through the main document to trigger viewport-based loading.
    // The iteration cap prevents an endlessly growing page from looping forever.
    await page.evaluate(async () => {
      const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
      const step = Math.max(300, Math.floor(window.innerHeight * 0.75));
      let previousHeight = 0;
      let stablePasses = 0;

      for (let pass = 0; pass < 40; pass++) {
        const height = document.documentElement.scrollHeight;
        for (let y = 0; y < height; y += step) {
          window.scrollTo(0, y);
          await pause(150);
        }
        await pause(300);

        const newHeight = document.documentElement.scrollHeight;
        if (newHeight === previousHeight) stablePasses++;
        else stablePasses = 0;
        previousHeight = newHeight;

        if (stablePasses >= 2) break;
      }
      window.scrollTo(0, 0);
    });

    // Replace this with a page-specific assertion when you know the expected
    // lower-page content. Visibility of main alone does not prove every image loaded.
    await page.locator('main').waitFor({ state: 'visible' });

    const images = await page.locator('img').evaluateAll(elements =>
      elements.map(img => ({
        src: img.currentSrc || img.src,
        complete: img.complete,
        naturalWidth: img.naturalWidth
      }))
    );
    console.log(JSON.stringify(images, null, 2));

    await page.screenshot({ path: 'wordpress-page.png', fullPage: true });
  } finally {
    await browser.close();
  }
})().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

The scroll distance and pauses are practical starting values, not guarantees. A very long page, slow image host, or JavaScript-heavy theme may need different settings. The height-stability loop helps with pages that grow during scrolling, but infinite feeds need a site-specific stopping condition such as a known final item or a maximum item count.

Why full-page capture alone can miss lazy-loaded images

Lazy loading commonly delays requesting an image until it is near the viewport. A full-page screenshot expands the capture extent to the whole scrollable document, but that is separate from scrolling through the page to trigger the site’s loading behavior. See the [Playwright Page API](https://playwright.dev/docs/api/class-page) for the screenshot options.

Navigation readiness is separate too. Playwright’s navigation guidance explains the document lifecycle and that the load event waits for dependent resources such as images. Content that only gets requested after scrolling may still need the scroll-and-wait step. See [Playwright navigation](https://playwright.dev/docs/navigations).

Choose navigation and readiness waits

Choice What it tells you When to use it
domcontentloaded The initial document has been parsed. Useful when you plan to perform your own content and resource checks.
load The document and dependent resources have reached the load event. Useful when initial images and resources should be loaded before continuing.
Locator wait or web-first assertion A specific element or expected page condition is present. Prefer this for confirming the content your capture needs.
networkidle No network connections for a quiet interval. Do not use it as a universal readiness signal; persistent analytics, polling, and other requests make it unreliable.

The Playwright Page API explicitly discourages using networkidle for tests and recommends relying on web assertions to assess readiness. A quiet network does not prove a particular image, heading, or lower-page section is correct. Conversely, a page can be ready for capture while analytics or other background requests continue. Use a locator or assertion for the content that matters.

Make the readiness check specific to the page

Waiting for main to be visible only confirms that the main region is visible. It does not confirm the expected article, its lower sections, or all images. A stronger check targets content you expect after scrolling:

const expectedSection = page.getByRole('heading', {
  name: 'Frequently asked questions',
  exact: true
});
await expectedSection.waitFor({ state: 'visible', timeout: 15_000 });

If a site has a known image or content count, check that too. For example, use a locator for a known image and evaluate its natural dimensions:

const hero = page.locator('img.article-hero');
await hero.waitFor({ state: 'attached' });
await page.waitForFunction(() => {
  const img = document.querySelector('img.article-hero');
  return img && img.complete && img.naturalWidth > 0;
}, { timeout: 15_000 });

Use selectors that match the target site’s actual markup. A missing selector should fail visibly with a timeout rather than quietly producing a partial capture.

Inspect image loading before saving

The example logs each image’s URL, complete state, and naturalWidth. A positive natural width is evidence that the browser decoded an image successfully. It is not a check for every kind of visual content: CSS background images, iframe contents, video frames, and third-party widgets need their own checks.

You can fail the capture when ordinary image elements did not load, if that is the policy you need:

const failedImages = images.filter(img => !img.complete || img.naturalWidth === 0);
if (failedImages.length) {
  throw new Error(`Image check failed for ${failedImages.length} image(s)`);
}

Some pages intentionally contain broken or decorative images. Decide whether to fail, log, or allow those cases based on the content you are capturing.

Adapt the scroll for unusual WordPress pages

  • Infinite scroll: define an end condition, such as a known final article or two consecutive passes with unchanged item count and document height. Keep a maximum pass count or deadline so the capture terminates.
  • Nested scroll container: scroll the element that owns the content instead of window. Find the container from the page’s markup and advance its scrollTop in steps. A document scroll will not trigger observers tied to a separate scroller.
  • Consent overlay: determine whether the overlay blocks the target content. Use the site’s documented consent mechanism or test setup when available; do not assume the page behind an overlay is fully interactive.
  • Carousel or tabs: scrolling does not reveal slides or panels that require a click. Activate the required control and check the resulting content before capture.
  • Content keeps growing: the page may append items as you reach the bottom. Set an explicit maximum and stop when the expected content appears or growth stabilizes.

Capture options and output size

The basic call writes a PNG to disk:

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

Playwright’s screenshot API also supports screenshot scale options. A CSS-scale capture can reduce pixel dimensions; device scale preserves device-pixel scaling. Full-page images can become very tall and consume substantial memory, especially at high device scale. If the full document is not required, capture a specific locator or viewport instead. For very long pages, consider capturing sections separately and keeping a record of their order.

Set viewport size and device scale factor deliberately because responsive WordPress themes may render different layouts at different widths. For repeatable output, also control the browser version, fonts, locale, and any page state that affects rendering.

cURL, Python, and Node.js alternatives with ScreenshotNeo

Playwright is useful when you need browser automation, page-specific interactions, or custom validation. If you only need the screenshot artifact, ScreenshotNeo provides a website screenshot API and MCP server. Its API accepts a URL in one GET request and can return an image or PDF. See the ScreenshotNeo API documentation for request options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com/wordpress-page \
  -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/wordpress-page",
    },
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as output:
    output.write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/wordpress-page'
});
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 accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Responses identify the page verdict and billing status in headers. Its MCP server includes take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a 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 required.

Troubleshooting

Symptom Likely cause Fix
Images are missing in the screenshot The browser captured before scrolling triggered lazy loading, or the image request failed. Scroll in steps, wait for a page-specific condition, and inspect currentSrc, complete, and naturalWidth.
fullPage still shows blank areas Full-page capture extends the screenshot; it does not guarantee offscreen content was requested. Trigger the site’s loading behavior before capture and check the relevant image or content state.
networkidle never resolves Analytics, ads, polling, or other connections remain open or recur. Use a locator or assertion for the content needed by the screenshot.
The lower part of the page is absent Content is in a nested scroller, loads after a user action, or the page grows after the scroll pass. Scroll the correct container, interact with the required control, or repeat bounded passes until a defined stopping condition is met.
Wait for main succeeds but content is incomplete The locator checks visibility, not the full content or successful image loads. Assert on an expected heading, item count, or image state.
Screenshot is unexpectedly tall or large The full document and device scale produce many pixels. Use CSS scale, a smaller device scale factor, a target locator, or section captures if a full-page image is unnecessary.
Navigation times out The site is slow, blocked, or waiting for a lifecycle event that does not occur. Choose an appropriate navigation state, set a considered timeout, then wait for the specific page condition. Investigate access restrictions if the page cannot be reached.

Performance, reliability, and cost

Scrolling adds time because the browser must visit successive viewports and allow observers and image requests to run. Larger pauses can help slow pages but increase capture time; tune them against the site’s behavior and confirm results with checks rather than assuming a delay is sufficient. A long or dynamically expanding page can also increase screenshot memory use.

For reliability, set navigation and locator timeouts, bound scrolling loops, close the browser in a finally block, and record failed image checks. Use a stable viewport and browser version when comparing captures. If the job runs in CI, make browser installation and fonts part of the environment setup.

Running Playwright yourself means accounting for the environment that hosts the browser and the time spent on navigation, scrolling, and retries. ScreenshotNeo offers a free allowance of 1,000 shots per month without a card; paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Choose based on expected volume and whether browser setup or API calls better fit your workflow.

FAQ

Does fullPage: true trigger lazy loading?

It requests a screenshot of the full scrollable page. Do a scroll pass first when the site’s lazy loading depends on content approaching the viewport.

Should I wait for networkidle before taking the screenshot?

Usually not as a universal rule. Playwright discourages it for tests; wait for the specific content or resource state your capture requires.

Does the load event mean every lazy image is ready?

No. It covers resources loaded as part of the document’s load lifecycle, but images deferred until scrolling may not have been requested yet.

Can Playwright verify CSS background images?

The image-element checks in this guide cover <img> elements. Background images need separate inspection of the relevant element’s computed style and the referenced resource.