ScreenshotNeo

BlogHow-to

How to troubleshoot screenshots that show skeleton loaders instead of content

A screenshot can capture a page before its data arrives. Learn how to wait for real content, diagnose a skeleton that never clears, and make captures repeatable.

By the ScreenshotNeo team4 October 20268 min read

A screenshot that shows skeleton loaders may be capturing the page before its content has rendered or data has arrived. If the skeleton remains after the page settles, the application or the request that supplies its content may be failing. A screenshot shows a visual state; by itself, it cannot tell you which situation you have.

First check whether the expected content eventually appears in an ordinary browser session. If it does, make your capture wait for that content. If it does not, inspect the requests and browser errors that should produce it before changing screenshot timing.

1. Decide whether the screenshot is early or the page is stuck

Reproduce the capture with the same URL, browser, viewport, account state, and test data. Watch the page beyond the moment the screenshot is taken.

  • The content eventually appears: the capture may be happening during an intermediate rendering or data-loading state. Add a condition tied to the expected content.
  • The skeleton never changes: investigate the application, its data request, authentication, or environment. Increasing a timeout cannot make a failed request succeed.
  • The issue appears only in automation: compare browser and account state, test data, and console output. For server-rendered React under Cypress, a hydration error may point to a specific runner interaction; it is not a general explanation for every skeleton.

Cypress explains that a visual snapshot can capture an intermediate state while an app is rendering, animating, or waiting for data. Its guidance puts the essential point plainly: “Snapshot commands capture whatever is on screen at that moment.” [Cypress visual testing documentation](https://docs.cypress.io/app/guides/visual-testing).

2. Wait for the content the screenshot is supposed to show

Choose a meaningful condition: the page heading, a known table row, a loaded article body, or the content container that replaces the skeleton. Waiting for a navigation event or adding an arbitrary delay is not equivalent to confirming that the relevant data has rendered.

Cypress says cy.visit() waits for the page’s load event, but it does not automatically wait for every XHR or Ajax request. Identify the request your page depends on and wait for it where appropriate, then assert the rendered result. [Cypress waiting and retrying](https://docs.cypress.io/app/core-concepts/retry-ability#Aliases).

Playwright lists navigation conditions including commit, domcontentloaded, load, and networkidle. Its Page API discourages using networkidle as a testing readiness signal and recommends web assertions instead. An app can keep network connections open, or finish network activity before client-side rendering and data processing are complete. [Playwright Page API](https://playwright.dev/docs/api/class-page#page-goto).

Cypress: wait for a relevant request, then assert content

describe('article screenshot', () => {
  it('captures the article after its content appears', () => {
    cy.intercept('GET', '**/api/articles/42').as('article');
    cy.visit('/articles/42');

    cy.wait('@article').its('response.statusCode').should('eq', 200);
    cy.get('[data-cy="article-title"]')
      .should('be.visible')
      .and('not.have.text', '');

    cy.screenshot('article-loaded');
  });
});

Adjust the route pattern and selector to match the application. If the request can legitimately return an error, assert the expected status and handle that state explicitly rather than treating any response as loaded content. For repeatable visual checks, use stable fixtures or stub the relevant response as appropriate. Cypress recommends confirming that the page updated before taking a visual snapshot. [Cypress visual testing documentation](https://docs.cypress.io/app/guides/visual-testing).

Playwright: let a locator assertion establish readiness

import { test, expect } from '@playwright/test';

test('captures an article after its content appears', async ({ page }) => {
  await page.goto('https://example.com/articles/42');
  await expect(page.locator('[data-testid="article-title"]'))
    .toBeVisible();

  await expect(page.locator('[data-testid="article-body"]'))
    .not.toBeEmpty();

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

Replace the example URL and locators with your app’s values. Playwright’s locator assertions retry while checking the condition, so the screenshot follows a content-based readiness check rather than a guessed sleep. [Playwright assertions](https://playwright.dev/docs/test-assertions).

Browser-only manual check

  1. Open the same URL with the same account and data.
  2. Open developer tools and inspect the Network and Console panels.
  3. Identify which request should populate the skeleton area; check whether it fires, its status, and whether its response contains the expected data.
  4. Check whether the content appears only after scrolling, clicking, or another normal interaction.
  5. Compare the browser result with the automated capture using the same viewport and state.

3. Check lazy loading and visibility-triggered content

Some pages defer non-critical or off-screen content until it becomes visible. If the screenshot targets content below the fold, determine whether the page requires scrolling or another interaction to trigger loading. Scroll as a real user would, then assert the expected content before capture.

// Playwright example: bring a below-the-fold section into view first.
const section = page.locator('[data-testid="results"]');
await section.scrollIntoViewIfNeeded();
await expect(section.locator('[data-testid="result-row"]')).toHaveCount(1);
await page.screenshot({ path: 'results.png', fullPage: true });

A full-page screenshot does not necessarily reproduce every interaction that would trigger visibility-based loading. Google describes lazy loading as deferring non-critical or non-visible content and cautions that an incorrect implementation can hide content from Google. That makes visibility behavior worth checking; it does not establish that lazy loading is the cause of a particular screenshot. [Google Search Central: lazy-loading guidance](https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading).

4. Diagnose a skeleton that never clears

If the expected content does not appear, use the request and browser evidence to locate the failing step:

  1. Confirm the request is sent. If it is absent, check whether a user action, route transition, or visibility trigger is required.
  2. Check the response. Inspect status and payload. A successful HTTP response may still contain an application-level error or data different from what the UI expects.
  3. Check the browser console. Look for JavaScript exceptions, failed resource loads, or hydration errors that could stop rendering.
  4. Verify access and state. Compare authentication, cookies, permissions, environment, and test data with a working session.
  5. Check app rendering. If the data is present but the skeleton remains, investigate the client-side state transition and the component responsible for replacing the placeholder.

In Cypress with a server-rendered React app, a hydration error that occurs only in the runner may be related to Cypress injecting a bootstrap script before React hydrates. Cypress documents a data-cy-bootstrap marker workaround for that particular injection mismatch. Apply it only when the documented mechanism and corresponding hydration error fit the evidence. [Cypress hydration troubleshooting](https://docs.cypress.io/app/guides/troubleshooting#React-hydration-mismatch).

5. Make visual captures repeatable after the content is correct

A stable screenshot is useful only after the expected content has been asserted. Once the readiness condition passes:

  • Keep test data and account state consistent; use fixtures or stubbed responses where suitable.
  • Keep the browser, platform, viewport, and device scale consistent across runs.
  • Control animation and time-dependent content if they create irrelevant visual changes.
  • Wait for the particular content that matters, rather than every request on the page.
  • For below-the-fold content, trigger the normal visibility or interaction behavior and verify the result.

Playwright’s visual comparison guide describes taking captures until two consecutive screenshots match and saving the last one. This can stabilize a comparison, but matching images alone does not prove the intended content loaded; retain the content assertion. [Playwright visual comparisons](https://playwright.dev/docs/test-snapshots).

6. Common errors and fixes

Symptom Likely explanation to investigate Useful next step
Navigation finished, but the screenshot shows placeholders Data loading or client rendering continued after the navigation event. Wait for the relevant request and assert the expected content.
Increasing the timeout changes nothing The content request may fail, return unexpected data, or never be triggered. Inspect Network status and payload, then check console errors and access state.
Only content below the fold is missing Loading may depend on visibility or interaction. Scroll to the target or perform the normal interaction, then assert loaded content.
Tests are flaky despite content appearing Data, viewport, animation, time, or browser rendering conditions vary. Stabilize test data and environment and account for animation or dynamic content.
SSR React fails only in Cypress with a hydration error The runner’s injected bootstrap may affect hydration in the documented case. Verify the error mechanism and consult Cypress’s targeted data-cy-bootstrap guidance.

7. Performance, reliability, and cost considerations

Waiting for a specific request and a meaningful locator usually avoids both premature captures and unnecessary fixed delays. A very long global timeout can make genuine failures slower to diagnose, while a short timeout can fail on normal variation. Set timeouts to match the application’s expected behavior, and make timeout errors identify the content or request that did not become ready.

Network-idle conditions can be unreliable as a general readiness rule for apps with polling, analytics, streaming, or other ongoing connections. Prefer the smallest app-specific condition that proves the target content is ready. For visual regression, control inputs and rendering conditions so that image differences indicate meaningful changes rather than changing data or environment.

There is no universal cost or timing figure for this troubleshooting workflow. In CI, the practical tradeoff is between waiting long enough for legitimate rendering and spending time retrying a page whose request or rendering path is actually broken. Diagnose the failing request before raising the capture timeout.

8. Or skip the browser setup

If you need a clean capture of a URL without wiring up a browser automation script, ScreenshotNeo provides a website screenshot API and MCP server. Its API returns a screenshot or PDF from one GET request. For example, this cURL call saves a WebP capture:

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

See the ScreenshotNeo API documentation for the request options and response details. Cookie banners and consent prompts, newsletter popups, and chat widgets from supported platforms are removed before the shot; each removal step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status.

ScreenshotNeo also offers an MCP server with screenshot, page-info, and PDF-capture tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. It can capture many other page states too, including full pages, a selected element, dark mode, device viewports, and PDFs.

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

FAQ

Does a skeleton screen mean the website is broken?

No. It can be a normal intermediate state captured before content arrives, or it can persist because the data or rendering path failed. Check whether content eventually appears and inspect the relevant request and console.

Should I always wait for network idle?

No. Playwright discourages network idle as a test readiness condition. Assert the content the screenshot needs.

Can a full-page screenshot force lazy content to load?

Do not assume so. If the page loads content on visibility or interaction, trigger that behavior and verify the result before capture.

Why does the screenshot look stable but still show a skeleton?

Visual stability means the image stopped changing; it does not establish that the expected content arrived. Add a content assertion before the screenshot.