Recommended Free Tools
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Wait for two separate milestones before taking a screenshot in PHP: first, wait for the custom element name to be registered with customElements.whenDefined(); then wait for a visible, application-specific state that proves the component has finished the work your image must show. Registration upgrades the element, but it does not guarantee that data fetching, rendering, images, or animations are complete.
This distinction prevents screenshots that contain an upgraded component shell but miss its asynchronously loaded content. The examples below use Playwright from PHP and show how to choose a viewport, full-page, or element capture after the correct readiness condition.
The readiness sequence that avoids race conditions
- Navigate to the target URL.
- Wait for definition with
customElements.whenDefined('my-element'). - Wait for useful content: a meaningful child locator, expected text, or an explicit ready marker supplied by the component.
- Capture the smallest scope that answers your question: viewport, full page, or the component element.
A tag can already be present in the DOM while its class is not registered. Until registration, the browser treats it as an ordinary HTMLElement; after registration, it upgrades matching connected elements and runs the component’s lifecycle callbacks. Testing only for tag presence therefore allows a race.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsWhat customElements.whenDefined() actually guarantees
The method returns a promise that resolves when the named element is defined. If the definition already exists, the promise resolves immediately. In the words of the MDN Web Docs API description: “The whenDefined() method of the CustomElementRegistry interface returns a Promise that resolves when the named element is defined.”
#1 Best Overall
That promise is a definition barrier, not a data-readiness barrier. A component may start a fetch in connectedCallback(), render a loading state, measure itself, or wait for nested components after it has been defined. Your second wait must match the component’s actual contract.
One element name
await customElements.whenDefined('my-element');
Use a valid custom-element name containing a hyphen. The promise fulfills with the element constructor, but most screenshot workflows only need to know that upgrade is complete.
Several element names
If the page depends on multiple custom elements, collect unique names and wait for all of them. Waiting for only the first can leave another widget unupgraded.
const names = [...new Set(['site-shell', 'price-chart', 'review-list'])];
await Promise.all(names.map(name => customElements.whenDefined(name)));
Do not pass ordinary built-in names such as div; whenDefined() is for custom-element names.
A PHP Playwright implementation
Playwright’s PHP bindings provide navigation, locator waiting, assertions, and screenshot methods. The exact PHP wrapper for evaluating an asynchronous browser promise can differ by installed Playwright PHP version, so confirm the signature in the API reference for your package. The browser-side operation itself is stable; the following pattern shows the required order.
Definition wait followed by a component-specific locator
<?php
use PlaywrightPlaywright;
$playwright = Playwright::create();
$browser = $playwright->chromium()->launch([
'headless' => true,
]);
$page = $browser->newPage();
$page->goto('https://example.test/dashboard', [
'waitUntil' => 'domcontentloaded',
]);
// Evaluate JavaScript in the page and await the definition barrier.
$page->evaluate("async () => {
await customElements.whenDefined('analytics-panel');
}");
// This is the application's readiness contract, not a guessed delay.
$page->locator('analytics-panel [data-state=ready]')->waitFor([
'state' => 'visible',
]);
$page->screenshot([
'path' => 'dashboard.webp',
'fullPage' => true,
'type' => 'webp',
]);
$browser->close();
Check the installed binding’s method names and option casing before copying this verbatim. Some PHP releases expose assertion methods through an expect() API; the important behavior is the same: evaluate the browser promise, then wait on a concrete locator.
Rank #2
Waiting for text instead of a ready attribute
If the component has no explicit state marker, wait for text that cannot appear until the required data is rendered.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →$page->evaluate("async () => {
await customElements.whenDefined('profile-card');
}");
$card = $page->locator('profile-card');
$card->getByText('Account owner')->waitFor(['state' => 'visible']);
$card->screenshot(['path' => 'profile-card.png']);
Choose text or a child selector that represents the final state, not a permanent heading such as “Loading”. If the text can legitimately be absent, use another observable contract such as a populated list count or a data-ready attribute.
Waiting for several definitions from PHP
$names = ['site-shell', 'price-chart', 'review-list'];
$json = json_encode(array_values(array_unique($names)), JSON_THROW_ON_ERROR);
$page->evaluate("async (names) => {
await Promise.all(names.map(name => customElements.whenDefined(name)));
}", $names);
If your PHP binding accepts only a single expression argument, embed the JSON safely in the expression instead. Never concatenate untrusted page input into JavaScript; pass it as an evaluation argument where the binding supports that.
Choose the screenshot scope deliberately
| Capture | Use it when | Trade-off |
|---|---|---|
| Viewport | You need exactly what a user could see in the current viewport. | Below-the-fold content is omitted. |
| Full page | The evidence includes content below the fold. | Long pages can include more layout noise and require the page to settle throughout its height. |
| Element | You are documenting one custom widget or an unstable region. | Context outside the element is not captured. |
Use the element screenshot when the page contains unrelated animations, ads, or changing navigation. Use full-page capture only after lazy content needed by the image has been loaded. A screenshot records pixels; it is not a substitute for a locator assertion that proves text, visibility, enabled state, or count.
Why fixed sleeps are an unreliable substitute
A command such as sleep(2000) expresses no fact about the page. On a fast run it wastes time; on a slow run it expires before a network response or render finishes. Playwright generally auto-waits before actions, but automatic waiting cannot infer that your custom element’s business data is complete. Assert the state your component promises instead.
Free tools Windows power users keep installed
One-click scans. No signup required.
If the component has a documented event, ready attribute, or status element, use it. If it has none, add a stable test hook such as data-state="ready" rather than relying on a timing guess. Keep the selector narrow enough that an unrelated child cannot satisfy it.
Making the page deterministic before capture
Control navigation and network conditions
- Use a deterministic test URL or fixture when validating the capture pipeline.
- Set authentication, cookies, and headers before navigation if the component needs them.
- Wait for the specific data state instead of assuming
domcontentloadedmeans application readiness. - If images affect the result, wait for the relevant image locators and confirm their natural dimensions where practical.
Deal with animations and layout shifts
Disable nonessential CSS animations in the page or through a test stylesheet, and capture after the component’s final layout is visible. A ready marker should be set after data and layout work that matters to the screenshot, not merely at the start of a fetch.
Use an explicit timeout as a failure boundary
A timeout is useful for reporting a broken page, but it is not evidence that the page is ready. Set a bounded timeout on the locator wait, log the URL and missing selector on failure, and preserve the diagnostic HTML or trace if your test system supports it.
Common failures and fixes
“The element exists, but it shows a blank shell”
Cause: the tag was parsed before registration, or registration finished while its data request was still pending.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Fix: wait for whenDefined(), then wait for a final child locator or ready marker. Do not replace the second wait with a longer sleep.
“whenDefined() never resolves”
Cause: the script that calls customElements.define() failed, the name is misspelled, or the page never loads the module that defines it.
Fix: inspect browser console errors and network responses, verify the exact hyphenated name, and confirm that the definition runs on this route. A timeout should identify this as a registration failure.
Rank #4
“The ready selector times out”
Cause: the selector describes an implementation detail that changed, the component reports a different state, or the underlying request failed.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Fix: inspect the component’s DOM and contract, choose a stable user-visible condition, and separately check the request and error state. If an error state is legitimate, fail with that state rather than taking a misleading screenshot.
“The full-page image cuts off lazy content”
Cause: content below the initial viewport has not been activated or loaded.
Fix: trigger the page’s supported lazy-loading behavior, wait for the required content to become visible, and only then request a full-page screenshot. For a single widget, an element screenshot avoids unrelated lazy regions.
“The PHP call fails while the JavaScript works in DevTools”
Cause: the installed PHP binding exposes a different evaluate() signature or locator method.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Fix: consult the versioned Playwright PHP API for asynchronous evaluation and locator waits. Keep the browser-side expression—await customElements.whenDefined(...)—unchanged while adapting only the PHP wrapper.
Or skip the browser setup
For a one-call capture, ScreenshotNeo provides a website screenshot API and MCP server. It handles the browser session for you and can return PNG, JPEG, WebP, or PDF. Before capture, it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
Use the API documentation at https://screenshotneo.com/docs/ for the complete option set. The call below uses the requested target URL; replace the access key with your own.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
PHP client example
<?php
$r = file_get_contents(
'https://api.screenshotneo.com/v1/shot?' . http_build_query([
'access_key' => 'YOUR_API_KEY',
'url' => 'https://stripe.com',
])
);
file_put_contents('shot.webp', $r);
For production PHP, use your HTTP client’s timeout, status-code, and response-header handling so you can record X-Page-Verdict and X-Billed.
Python
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)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus arbitrary viewports, retina scale, PDF paper and page-range controls, custom CSS and JavaScript, pre-capture clicks, selector hiding, waits for selectors, delays or network idle, request and resource blocking, custom headers/cookies/user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, selectable-TTL caching, signed public-image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Its parameter names are compatible with those used by other screenshot APIs, which can simplify migration.
An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients, so an AI agent can perform the capture without your PHP process managing a browser.
| Plan | Included shots | Price |
|---|---|---|
| Free | 1,000 per month | $0, no card |
| Starter | 3,000 | $5 |
| Growth | 15,000 | $15 |
| Pro | 60,000 | $39 |
| Scale | 250,000 | $99 |
| Business | 1,000,000 | $249 |
Yearly billing provides two months free, and every feature is available on every plan. Start with 1,000 free screenshots a month with no card; paid plans start at $5 for 3,000.
A practical checklist
- Is the custom-element name exact and registered on this route?
- Did you await every unique relevant name?
- What observable state proves the component’s required content is complete?
- Does the selector represent final content rather than a loading shell?
- Should the evidence be viewport, full page, or element scope?
- Are lazy images, animations, authentication, and layout shifts controlled?
- Will a timeout report the missing state and preserve useful diagnostics?
- Would a ScreenshotNeo API or MCP call remove browser-maintenance work for this capture?
FAQ
Does whenDefined() wait for a component’s API request?
No. It waits only for registration. Add a wait for the component’s rendered data or explicit ready state.
Can I wait for the custom-element tag with a normal locator?
A locator proves the tag is present, not that it has been upgraded or populated. Use the definition barrier and then a meaningful state assertion.
Which screenshot mode is least noisy for a custom widget?
An element screenshot is usually the narrowest evidence. Choose viewport or full page when surrounding layout is part of what you need to document.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

