ScreenshotNeo

BlogHow-to

How to Capture Lazy-Loaded Content in a Cross-Origin Iframe

Scroll a lazy iframe into view, wait for the content you need, then capture it with browser automation. Learn what cross-origin security allows and how to troubleshoot blank captures.

By the ScreenshotNeo team4 October 20269 min read

Direct answer: Use browser automation to bring the iframe into or near the viewport, wait for the specific content inside it to be visible, and then capture the rendered page or inspect the frame through the automation API. A full-page screenshot does not guarantee that scrolling has activated lazy or scroll-triggered content. Ordinary JavaScript on the parent page cannot read a cross-origin iframe’s document because of the same-origin policy.

This guide uses Playwright with Node.js. It shows a reusable screenshot workflow, explains the difference between rendered pixels and HTML extraction, and covers what to do when the iframe belongs to another origin.

1. Know which kind of capture you need

Choose the output before writing the automation:

Need Approach Limit
A visual record Make the target content visible, then take a page or element screenshot. A screenshot contains pixels, not the underlying data. Content that was never activated may not appear.
Text or HTML from the frame Use Playwright frame APIs to locate and inspect the relevant content. Page JavaScript still cannot directly read another origin’s document. Automation frame access is a separate browser automation context.
Data from a frame you control Expose a deliberate postMessage interface from the frame. Validate the sender origin and message shape on both sides.

First confirm that the provider allows the page to embed the frame. Policies such as X-Frame-Options can prevent framing altogether. An embedded frame is not automatically readable by its parent.

2. Set up Playwright

Use a real browser session. The following example uses Playwright’s Node.js package and Chromium. Create a project and install the dependency:

npm init -y
npm install playwright
npx playwright install chromium

Save the script below as capture-frame.mjs. Replace the sample page URL, iframe selector, and target selector with values from the page you are authorized to access.

3. Scroll, wait for the target, and capture

import { chromium } from 'playwright';

const pageUrl = 'https://example.com/article';
const iframeSelector = 'iframe[data-testid="report-frame"]';
const targetSelector = '.report-chart';
const outputPath = 'iframe-capture.png';

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });

try {
  await page.goto(pageUrl, { waitUntil: 'domcontentloaded' });

  const iframe = page.locator(iframeSelector);
  await iframe.waitFor({ state: 'attached' });
  await iframe.scrollIntoViewIfNeeded();

  const frame = page.frameLocator(iframeSelector);
  const target = frame.locator(targetSelector);
  await target.waitFor({ state: 'visible', timeout: 30000 });

  // Capture the rendered parent page with the target visible.
  await page.screenshot({ path: outputPath, fullPage: false });
  console.log(`Saved ${outputPath}`);
} finally {
  await browser.close();
}

Run it with node capture-frame.mjs. The wait is tied to the content you want, rather than a fixed sleep or the page’s generic load event. The iframe’s loading="lazy" behavior is based on its distance from the visual viewport, so scrolling the parent page is the activation step.

If you need the whole parent page after the target has loaded, change the screenshot call to await page.screenshot({ path: outputPath, fullPage: true }). Full-page mode captures the page’s scrollable area; do not assume it scrolls through the page in a way that triggers every application’s scroll handlers or virtualized content.

4. Inspect frame content instead of taking a screenshot

For text or HTML, use Playwright’s frame APIs after the frame has loaded. This example waits for the target and retrieves its text:

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();

try {
  await page.goto('https://example.com/article', { waitUntil: 'domcontentloaded' });
  const iframeSelector = 'iframe[data-testid="report-frame"]';
  await page.locator(iframeSelector).scrollIntoViewIfNeeded();

  const frame = page.frameLocator(iframeSelector);
  const target = frame.locator('.report-chart');
  await target.waitFor({ state: 'visible', timeout: 30000 });
  console.log(await target.innerText());

  // To retrieve the frame's serialized HTML, identify the Frame from page.frames().
  const frameUrl = 'https://embed.example.net/report';
  const matchingFrame = page.frames().find((candidate) => candidate.url().startsWith(frameUrl));
  if (!matchingFrame) throw new Error(`Frame not found: ${frameUrl}`);
  console.log((await matchingFrame.content()).slice(0, 1000));
} finally {
  await browser.close();
}

For frames whose URL or navigation timing is variable, listen for frame attachment/navigation or inspect page.frames() after bringing the iframe into view. Match on a stable URL prefix, frame name, or other known property; avoid assuming that array position stays constant. frameLocator is useful for locator operations, while Frame.content() returns a frame’s HTML.

5. Cross-origin security and page-owned scripts

The browser same-origin policy prevents parent-page JavaScript from freely reading a different-origin frame’s document. Code like this will fail or return no usable document when the frame is cross-origin:

// Do not use this to read a cross-origin document from the parent page:
const doc = document.querySelector('iframe').contentDocument;

Use browser automation’s frame tree when operating a Playwright browser session. That is not the same as granting ordinary page JavaScript permission to bypass browser security.

If you control both the parent and iframe applications, define a narrow message protocol instead. The receiver should check the exact expected origin, message type, and payload shape:

// Parent page, after obtaining a reference to the iframe window:
const frameWindow = document.querySelector('#report-frame').contentWindow;
frameWindow.postMessage({ type: 'REQUEST_REPORT', version: 1 }, 'https://embed.example.net');

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://embed.example.net') return;
  if (event.source !== frameWindow) return;
  if (!event.data || event.data.type !== 'REPORT_READY' || event.data.version !== 1) return;
  console.log(event.data.payload);
});

The iframe must implement the matching listener and send its response to the parent’s exact origin. Do not use '*' as the target origin for sensitive messages. If you do not control the frame, check whether its provider offers an API or export; the correct option depends on that provider.

6. Wait for the right state

A navigation completing, an iframe’s load event, or a quiet network period does not prove that the target chart, image, or text is ready. Prefer an observable condition for the specific content:

  • Wait for a stable target selector to be visible when you need a screenshot.
  • Wait for the target to exist and then check its text or attributes when extracting data.
  • If the application updates the same node after rendering, wait for an expected value or application-specific ready marker.
  • Use a timeout as a failure boundary and report which condition timed out.

Playwright discourages using networkidle as a general test-readiness condition. Pages can continue making background requests, and a quiet network does not establish that the desired visual state exists. A short fixed delay can help diagnose timing, but it is a brittle final readiness rule.

7. Screenshots with cURL, Python, and Node.js

A command-line HTTP screenshot service can capture a page without installing and managing a local browser. These examples request a screenshot of a page; they do not configure scrolling inside a third-party iframe or guarantee that application-specific lazy content has activated. For that, use the Playwright workflow above or a service that supports the required wait and interaction options.

cURL

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

Python

import requests

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

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/article',
});
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('article.webp', new Uint8Array(await res.arrayBuffer()));

See the ScreenshotNeo API documentation for supported parameters. ScreenshotNeo supports custom CSS and JavaScript, selector waits, delays, and network-idle waits among its capture options; confirm the exact parameter names and behavior in the docs before adapting a request. A one-call capture is useful when the page’s normal render is sufficient. If you must explicitly scroll the page, observe an iframe’s internal target, or handle provider-specific frame state, use a browser automation workflow that can express those steps.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. For a page that is ready to capture on load, one GET request returns an image or PDF:

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

Cookie banners are accepted and removed before capture, and known newsletter popups and chat widgets are removed; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response indicates 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 screenshots.

Create a free ScreenshotNeo account to get 1,000 screenshots per month with no card.

9. Troubleshooting

Symptom Likely cause Fix
The iframe is blank in the screenshot. It has not reached the viewport activation distance, has not navigated, or its target has not rendered. Scroll the iframe into view, wait for the frame and the specific target, then inspect the saved image.
Parent JavaScript throws a security error reading contentDocument. The frame is cross-origin. Use browser automation frame APIs, a provider API, or a validated postMessage protocol when you control both sides.
frameLocator times out. The iframe selector is wrong, the iframe has not attached, navigation is delayed, or the target selector is absent. Verify the iframe selector in the page, wait for attachment, inspect page.frames(), and confirm the target selector within the frame.
The iframe cannot be embedded. The provider may block framing with response headers or application policy. Use the provider’s permitted integration, API, or export. Browser automation cannot override a site’s framing policy.
The screenshot is taken but a chart or image is missing. The target is still loading, appears only after interaction, or is virtualized/scroll-triggered. Wait for a visible target or application-ready marker. Perform the required interaction or scroll explicitly before capture.
networkidle never resolves or resolves too soon. Background requests keep running, or network quiet happens before the target is rendered. Replace it as the readiness check with a selector or state tied to the content you need.
The result changes between runs. Viewport, authentication, consent, timing, or dynamic page content differs. Record the target URL, browser, viewport, authentication state, and wait condition; use a stable semantic selector.
Screenshot service returns an error or unexpected output. The URL may require access or custom rendering, or the chosen service parameters may not match the page behavior. Check the response and service documentation. Use browser automation when explicit frame interaction or inspection is required.

10. Reliability, performance, and cost

  • Reliability: Make the workflow fail clearly when the iframe or target does not appear. Use explicit timeouts, stable selectors, and a saved screenshot or extracted value as the artifact to inspect. Record browser version, viewport, target URL, and readiness condition for repeat runs.
  • Performance: Wait for the one target you need instead of every resource on the page. Capture only the needed viewport or element when a full-page image is unnecessary. Avoid repeated polling with very short intervals and avoid treating a fixed sleep as proof of readiness.
  • Cost: Local Playwright avoids per-request screenshot API charges but uses browser compute and requires maintenance of the browser environment. Hosted APIs trade browser setup for service usage. ScreenshotNeo offers 1,000 shots/month free, then plans from $5 for 3,000; yearly billing gives two months free. Every feature is on every plan. Only clean shots are billed, including no charge for bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits.
  • Security: Use only URLs and accounts you are authorized to access. Avoid logging secrets or sensitive frame contents, and validate origins and message payloads in any cross-origin message protocol.

11. Frequently asked questions

Does fullPage: true trigger lazy iframe loading?

Do not rely on it to run scroll-triggered application code. Bring the iframe into view and wait for its content before taking the full-page screenshot.

Can the parent page read a cross-origin iframe with JavaScript?

Not directly. The same-origin policy restricts that access. Use an explicit messaging interface if you control both applications, or use an appropriate automation or provider API.

Does a screenshot give me the iframe’s HTML?

No. A screenshot records rendered pixels. Use frame inspection or a provider data interface when you need structured content.

What if I do not control the iframe provider?

Scroll and wait through browser automation, provided the provider permits embedding and the session can access the content. Otherwise use the provider’s supported API or export.