What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a render-aware signal, not a fixed delay. On Android, wait for WebViewClient.onPageFinished(), then call postVisualStateCallback() and capture only from that callback. Android documents that onPageFinished() alone does not guarantee the next drawn frame reflects the current DOM. On Apple platforms, use WKWebView‘s asynchronous takeSnapshot completion handler, and add a page-specific ready signal when your JavaScript continues changing the page.

What “loaded” means for a screenshot

A navigation can be complete while the frame you capture is still displaying an older or incomplete visual state. Treat these as separate milestones:

  • Navigation completion: the main document response and its navigation lifecycle have completed.
  • DOM readiness: the document contains the nodes your script expects.
  • Render readiness: WebView has produced a frame that reflects that DOM.
  • Application readiness: your own JavaScript has finished fetching data, replacing placeholders, starting fonts, or applying a final layout.

A screenshot needs the last milestone relevant to the image, followed by the platform’s screenshot operation. A 500 ms sleep, document.readyState, or a navigation-finished callback by itself is not a universal guarantee.

Android WebView: wait for the visual-state callback

Android’s WebViewClient API reference explicitly warns that receiving onPageFinished() does not guarantee that the next frame drawn by WebView reflects the DOM at that point. The documented sequence is therefore:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Observe onPageFinished() for the main frame.
  2. Request postVisualStateCallback().
  3. Take the screenshot from the supplied visual-state callback.

Kotlin example

class CaptureActivity : AppCompatActivity() {
    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        webView = WebView(this)
        setContentView(webView)

        webView.settings.javaScriptEnabled = true // only if the page needs JavaScript
        webView.webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView, url: String) {
                super.onPageFinished(view, url)

                // This callback is the render boundary; do not capture above it.
                view.postVisualStateCallback(0L) {
                    captureWebView(view)
                }
            }
        }
        webView.loadUrl("https://example.com")
    }

    private fun captureWebView(view: WebView) {
        if (view.width == 0 || view.height == 0) return
        val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        view.draw(canvas)
        // Save or process bitmap here, off the UI thread after copying its pixels.
    }
}

Keep the callback and the WebView on the UI thread. A view also needs a measured, non-zero size; loading a URL into a hidden or unmeasured view can produce an empty image even when navigation succeeded. If you enable JavaScript, do so deliberately and apply the usual WebView security controls for the URLs you load.

Why not use onPageCommitVisible()?

onPageCommitVisible() is useful for knowing that response content has entered the DOM and that old-page content will no longer be drawn. It is an early visibility transition, not an all-resources-ready event. Android notes that linked CSS and images may still be unavailable at that point. Use it to prevent stale content during navigation, not as the final screenshot trigger.

Handling JavaScript-driven pages on Android

The visual-state callback tells you that the current DOM can be rendered; it does not know whether your application has finished its own asynchronous work. A dashboard may still be waiting for an API response, a chart library may still be painting, or a web font may still alter line wrapping.

When you control the page, expose an explicit readiness condition after the exact content required in the screenshot exists. For example, have the page add a marker:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
window.renderReport = async function () {
  await loadReportData();
  document.querySelector('#report').classList.add('ready');
};

Then poll or subscribe from the app, and only after the marker is present request the visual-state callback. A simple JavaScript evaluation can check the marker:

view.evaluateJavascript("document.documentElement.dataset.captureReady === 'true'") { value ->
    // Validate the value, then request postVisualStateCallback before drawing.
}

Prefer a deterministic page signal over guessing a delay. If you do not own the page, choose a specific selector or visible state that represents the content you need and enforce a timeout so a missing condition cannot hang the capture forever.

Apple WKWebView: use asynchronous takeSnapshot

Apple’s WKWebView documentation provides navigation delegates, JavaScript evaluation, and the asynchronous takeSnapshot API. The completion handler supplies the image when the snapshot operation is complete. Apple also documents that embedded resources such as images and videos are loaded as part of the initial load request, but that is not a promise that later JavaScript, animations, or application work has settled.

Swift example

import WebKit

final class CaptureViewController: UIViewController, WKNavigationDelegate {
    private var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()
        webView = WKWebView(frame: view.bounds)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        webView.navigationDelegate = self
        view.addSubview(webView)
        webView.load(URLRequest(url: URL(string: "https://example.com")!))
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        // If your page has asynchronous app work, wait for its own ready signal here.
        let configuration = WKSnapshotConfiguration()
        webView.takeSnapshot(with: configuration) { image, error in
            guard let image = image, error == nil else {
                // Handle the error and decide whether a bounded retry is appropriate.
                return
            }
            // Store or display image on the main thread.
            self.handle(image)
        }
    }

    private func handle(_ image: UIImage) {
        // Encode as PNG/JPEG or write to your app's storage.
    }
}

The navigation delegate’s didFinish tells you navigation has finished; the snapshot completion handler tells you the image is available. For a page you own, make the page set a marker such as document.documentElement.dataset.captureReady after data and layout work finish, check it with evaluateJavaScript, then call takeSnapshot. Keep that page-specific condition separate from the snapshot API’s completion semantics.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A reliable readiness sequence

For either platform, define what the image must contain before writing code.

  1. Load the URL or HTML into a visible, correctly sized WebView.
  2. Wait for the platform navigation event.
  3. If the page performs known asynchronous work, wait for its explicit marker, selector, or state. Use a deadline and report a timeout.
  4. Use the platform render/snapshot mechanism: Android’s postVisualStateCallback(), or Apple’s takeSnapshot.
  5. Save the image only after the callback returns successfully.

For animated content, decide whether the screenshot should capture a stable frame, a particular animation phase, or a disabled-animation state. If exact visual regression matters, make the page deterministic: fixed data, stable fonts, known viewport, and an explicit ready marker.

Troubleshooting common failures

Symptom Likely cause Fix
Screenshot shows old or partial DOM Capture ran directly in onPageFinished() or before the snapshot completion. On Android, request postVisualStateCallback(); on Apple, use takeSnapshot‘s completion handler.
CSS or images are missing Capture was triggered from onPageCommitVisible() or before resources were available. Treat commit-visible as an early transition only. Wait for the visual-state boundary and, when possible, a page-owned ready condition.
Dynamic data is absent Navigation finished before fetches or client rendering completed. Add a page-specific marker/selector after the data is rendered; enforce a timeout for missing data.
Blank or tiny image The WebView has not been measured, has zero dimensions, or is detached. Capture a laid-out view with an explicit size and verify width and height before drawing.
JavaScript page never updates JavaScript is disabled in Android WebView. Enable it only when required: webView.settings.javaScriptEnabled = true.
Intermittent differences between runs Animations, time-dependent data, ads, fonts, or network responses vary. Freeze inputs where possible, wait on an application-ready marker, and disable or control animation for comparison captures.
Capture waits forever The expected selector or readiness marker is never produced. Use a deadline, log navigation and readiness states, and return a useful timeout error instead of an unbounded wait.

Performance, reliability, and resource considerations

  • Do not block the UI thread. Navigation, callbacks, and view drawing are UI-related; encode, upload, or compare the resulting pixels off the main thread after you have copied them safely.
  • Control capture size. Large viewport dimensions and high-density output consume more memory. Capture only the region and scale the output your use case requires.
  • Use bounded retries. Retry only recoverable navigation or snapshot errors, and record whether the page failed, timed out, or never met its readiness condition.
  • Separate platform readiness from page readiness. A successful API callback cannot repair a page whose own data request failed.
  • Respect lifecycle changes. Cancel or invalidate a pending capture when its view controller or activity is destroyed, and do not write a result into a recycled screen.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you need server-side captures rather than an in-app WebView, ScreenshotNeo provides a single GET request for PNG, JPEG, WebP, or PDF output. Its cleanup step accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

Use the API documentation at https://screenshotneo.com/docs/ for all options, including selectors, waits, custom JavaScript, device presets, PDFs, signed links, asynchronous jobs, and bulk capture. A minimal call is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

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’s Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Every feature is included on every plan. Create a free ScreenshotNeo account to try it.

Choosing the right approach

Need Best fit
Screenshot inside an Android app WebView navigation event, then postVisualStateCallback(), with a page-specific ready condition when needed.
Screenshot inside an Apple app WKWebView navigation delegate plus asynchronous takeSnapshot; add an app-owned readiness signal for dynamic content.
Repeatable server-side captures or AI-agent workflows ScreenshotNeo, which handles cleanup, waiting and capture through an API and MCP server.

Frequently Asked Questions

Does Android’s onPageFinished() mean all images are visible?

No. Android says it does not guarantee that the next drawn frame reflects the DOM. Request postVisualStateCallback(), and add a page-specific condition if your content is still loading asynchronously.

Is document.readyState === 'complete' enough?

Not universally. It describes document loading, not necessarily your application’s later data rendering, animations, or the exact frame WebView will draw.

Can I use one callback implementation for Android and WKWebView?

No. Android uses postVisualStateCallback() as its documented visual-state signal; WKWebView provides asynchronous takeSnapshot. Keep the platform-specific lifecycle code separate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.