Screenshotting Dynamic Content: Mastering AJAX & Wait Times with Scrnify

4/17/2025

Hey there! Laura and Heidi here from SCRNIFY! 🇦🇹

Have you ever tried to capture a screenshot of a modern web application only to find that half the content is missing? Or perhaps you've built an automation script that consistently captures blank forms instead of populated data? If so, you're not alone. The challenge of capturing dynamic content loaded via JavaScript and AJAX is a common frustration for developers.

We've been there too! While building SCRNIFY, we spent countless hours tackling this exact problem. That's why we're excited to share our solution: Scrnify's powerful waitUntil parameter that gives you precise control over when screenshots are captured.

In this guide, we'll dive deep into how you can master the art of capturing dynamic content with our API. Let's get started!

Get free access to the SCRNIFY API during our open beta and start generating screenshots today! scrnify.com

Understanding the Problem: Why Dynamic Content Is Hard to Capture

Modern web applications rarely load all their content at once. Instead, they use JavaScript and AJAX to fetch data asynchronously after the initial page load. This creates a challenge for screenshot tools, which often capture images too early in the loading process.

Here's what typically happens:

  1. Your screenshot tool loads the URL
  2. The initial HTML loads quickly
  3. The tool takes a screenshot immediately
  4. JavaScript runs and AJAX requests complete after the screenshot is taken
  5. Result: A screenshot missing critical content 😅

This problem affects single-page applications (SPAs), dashboards, data visualizations, and any content that loads dynamically. The solution? We need a way to tell the screenshot tool to wait until the right moment before capturing.

Introducing Scrnify's waitUntil Parameter

Scrnify's API includes a powerful waitUntil parameter that gives you precise control over when screenshots are captured. This parameter allows you to specify exactly which page loading event should trigger the screenshot.

Here's the full list of options available:

waitUntil Option Description Best For
firstPaint When the first pixel is painted by the browser Ultra-fast captures of minimal content
firstContentfulPaint When the first text, image, canvas, or SVG is painted Basic text content
firstImagePaint When the first image element is painted Image-focused content
firstMeaningfulPaintCandidate When the browser first considers a meaningful paint might have occurred General purpose
firstMeaningfulPaint (default) When the primary content of the page is visible Most websites
DOMContentLoaded When initial HTML is completely loaded and parsed Static content
load When the whole page and all dependent resources are loaded Complete static sites
networkIdle When there are no more than 0 network connections for at least 500ms Dynamic SPAs
networkAlmostIdle When there are no more than 2 network connections for at least 500ms Sites with background activity

Let's explore when and how to use each option.

Choosing the Right waitUntil Option

The Early Events: Paint-based Options

The first group of options (firstPaint, firstContentfulPaint, firstImagePaint, firstMeaningfulPaintCandidate, and firstMeaningfulPaint) are based on browser paint events.

// Example: Capturing at firstContentfulPaint
const screenshotUrl = 'https://api.scrnify.com/capture'
  + '?key=YOUR_API_KEY'
  + '&url=' + encodeURIComponent('https://example.com')
  + '&type=image'
  + '&format=png'
  + '&width=1920'
  + '&height=1080'
  + '&waitUntil=firstContentfulPaint';

These options are useful when:

  • You need extremely fast screenshots
  • You're capturing static content that appears early
  • You're testing above-the-fold rendering performance

However, these options will often miss dynamically loaded content, so they're not ideal for AJAX-heavy applications.

The Middle Ground: DOM and Load Events

The next two options (DOMContentLoaded and load) correspond to standard browser events:

// Example: Capturing at load event
const screenshotUrl = 'https://api.scrnify.com/capture'
  + '?key=YOUR_API_KEY'
  + '&url=' + encodeURIComponent('https://example.com')
  + '&type=image'
  + '&format=png'
  + '&width=1920'
  + '&height=1080'
  + '&waitUntil=load';
  • DOMContentLoaded: This fires when the HTML is fully parsed and all synchronous scripts have executed. It's faster than load but may miss content from external resources.
  • load: This fires when all resources (images, stylesheets, etc.) have finished loading. It's more comprehensive than DOMContentLoaded but still may not capture AJAX content that loads after the initial page load.

These options work well for traditional websites but fall short for modern SPAs and AJAX-heavy applications.

The Dynamic Content Champions: Network Idle Options

For dynamic content loaded via AJAX, the network idle options are your best friends:

// Example: Capturing when network is completely idle
const screenshotUrl = 'https://api.scrnify.com/capture'
  + '?key=YOUR_API_KEY'
  + '&url=' + encodeURIComponent('https://example.com/dashboard')
  + '&type=image'
  + '&format=png'
  + '&width=1920'
  + '&height=1080'
  + '&waitUntil=networkIdle';
  • networkIdle: Waits until there are no network connections for at least 500ms. This is perfect for capturing fully loaded SPAs and AJAX content.
  • networkAlmostIdle: Waits until there are no more than 2 network connections for at least 500ms. This is useful for sites that maintain some background connections.

These options are ideal for:

  • React, Angular, or Vue.js applications
  • Dashboards that load data asynchronously
  • Any page with content loaded via fetch() or XMLHttpRequest

Real-World Example: Capturing a Dynamic Dashboard

Let's walk through a practical example of capturing a data dashboard that loads content dynamically:

// Node.js example using axios
const axios = require('axios');
const fs = require('fs');

async function captureDynamicDashboard() {
  try {
    // Configure the screenshot request with networkIdle
    const response = await axios({
      method: 'get',
      url: 'https://api.scrnify.com/capture',
      params: {
        key: 'YOUR_API_KEY',
        url: 'https://example.com/dashboard',
        type: 'image',
        format: 'png',
        width: 1920,
        height: 1080,
        waitUntil: 'networkIdle',  // Key for dynamic content!
        cache_ttl: 60  // Cache for 1 minute
      },
      responseType: 'arraybuffer'
    });

    // Save the screenshot
    fs.writeFileSync('dashboard.png', response.data);
    console.log('Dashboard screenshot captured successfully!');
  } catch (error) {
    console.error('Error capturing screenshot:', error);
  }
}

captureDynamicDashboard();

This code will:

  1. Make a request to the dashboard URL
  2. Wait for all network activity to complete (including AJAX requests)
  3. Capture the fully loaded dashboard
  4. Save it as a PNG file

When waitUntil Isn't Enough: Advanced Techniques

Sometimes even networkIdle isn't enough to capture certain types of dynamic content. Here are some additional techniques to handle challenging scenarios using Scrnify's current capabilities:

1. Handling Lazy-Loaded Content

Some websites only load content when it's scrolled into view. For these cases, consider:

// Capture a full page screenshot to trigger lazy loading
const screenshotUrl = 'https://api.scrnify.com/capture'
  + '?key=YOUR_API_KEY'
  + '&url=' + encodeURIComponent('https://example.com/infinite-scroll')
  + '&type=image'
  + '&format=png'
  + '&width=1920'
  + '&fullPage=true'  // Capture full page height
  + '&waitUntil=networkIdle';

2. Modifying the Target URL

For content that requires specific states or parameters, modify the URL to include query parameters that trigger the desired state:

// Example: Capturing a page with specific query parameters
const axios = require('axios');
const fs = require('fs');

async function captureWithQueryParams() {
  try {
    // Add query parameters to show specific content
    const targetUrl = 'https://example.com/dashboard?showAllData=true&expandSection=analytics';

    const response = await axios({
      method: 'get',
      url: 'https://api.scrnify.com/capture',
      params: {
        key: 'YOUR_API_KEY',
        url: encodeURIComponent(targetUrl),
        type: 'image',
        format: 'png',
        width: 1920,
        height: 1080,
        waitUntil: 'networkIdle'
      },
      responseType: 'arraybuffer'
    });

    fs.writeFileSync('dashboard-expanded.png', response.data);
    console.log('Screenshot with query parameters captured!');
  } catch (error) {
    console.error('Error:', error);
  }
}

This approach works well when:

  • The site accepts URL parameters to control state
  • You need to capture a specific filtered view
  • You want to bypass intro screens or tutorials

3. Using Custom User-Agent Strings

Sometimes content appears differently based on the device or browser. Scrnify allows you to customize the User-Agent:

// Example: Capturing a mobile view using a mobile User-Agent
const mobileUserAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1';

const screenshotUrl = 'https://api.scrnify.com/capture'
  + '?key=YOUR_API_KEY'
  + '&url=' + encodeURIComponent('https://example.com/responsive-content')
  + '&type=image'
  + '&format=png'
  + '&width=375'
  + '&height=812'
  + '&waitUntil=networkIdle'
  + '&userAgent=' + encodeURIComponent(mobileUserAgent);

This technique is useful for:

  • Testing responsive designs
  • Capturing mobile-specific content
  • Accessing content that's only available on certain devices

4. Combining Multiple Screenshots for Complex Workflows

For complex user journeys or multi-step processes, you might need to capture a series of screenshots:

// Example: Capturing a multi-step process
const axios = require('axios');
const fs = require('fs');

async function captureMultiStepProcess() {
  // Step 1: Capture the initial state
  const step1Response = await axios({
    method: 'get',
    url: 'https://api.scrnify.com/capture',
    params: {
      key: 'YOUR_API_KEY',
      url: 'https://example.com/checkout',
      type: 'image',
      format: 'png',
      width: 1920,
      height: 1080,
      waitUntil: 'networkIdle'
    },
    responseType: 'arraybuffer'
  });

  fs.writeFileSync('checkout-step1.png', step1Response.data);

  // Step 2: Capture after form submission (using a URL with query parameters)
  const step2Response = await axios({
    method: 'get',
    url: 'https://api.scrnify.com/capture',
    params: {
      key: 'YOUR_API_KEY',
      url: 'https://example.com/checkout?form=submitted&step=2',
      type: 'image',
      format: 'png',
      width: 1920,
      height: 1080,
      waitUntil: 'networkIdle'
    },
    responseType: 'arraybuffer'
  });

  fs.writeFileSync('checkout-step2.png', step2Response.data);

  console.log('Multi-step process captured successfully!');
}

💡 Pro Tip: For extremely complex scenarios requiring custom JavaScript execution before taking screenshots, reach out to the Scrnify support team at support@scrnify.com. They're actively developing new features based on user feedback!

Debugging Dynamic Content Screenshots

When your screenshots aren't capturing the expected content, try these debugging steps:

  1. Try different waitUntil options: Start with networkIdle and work backward to identify which stage is missing your content.

  2. Inspect network activity: Use browser DevTools to observe when your content actually loads. Look for delayed API calls or websocket connections.

  3. Check for lazy loading: Some content only loads when scrolled into view. Try using fullPage=true to trigger this content.

  4. Look for custom loading indicators: Many applications have custom loading states that don't correlate with network activity. You might need to wait for specific elements.

  5. Test with increasing delays: If all else fails, test with increasing delays to find the minimum time needed for your content to appear.

Case Study: Capturing a React Dashboard with Real-Time Updates

Let's examine a real-world scenario: capturing a React dashboard that loads data in multiple stages:

  1. Initial HTML loads (fast)
  2. React framework bootstraps (medium)
  3. API calls fetch dashboard data (slow)
  4. WebSocket connection provides real-time updates (continuous)

Here's how we'd approach this with Scrnify:

const axios = require('axios');
const fs = require('fs');

async function captureReactDashboard() {
  try {
    // First attempt: capture with networkIdle
    const response = await axios({
      method: 'get',
      url: 'https://api.scrnify.com/capture',
      params: {
        key: 'YOUR_API_KEY',
        url: 'https://example.com/react-dashboard',
        type: 'image',
        format: 'png',
        width: 1920,
        height: 1080,
        waitUntil: 'networkIdle',
        cache_ttl: 30  // Short cache time due to real-time nature
      },
      responseType: 'arraybuffer'
    });

    fs.writeFileSync('react-dashboard.png', response.data);
    console.log('React dashboard captured successfully!');

    // For a dashboard with continuous updates, you might want to
    // capture periodic screenshots:
    setInterval(async () => {
      const updateResponse = await axios({
        method: 'get',
        url: 'https://api.scrnify.com/capture',
        params: {
          key: 'YOUR_API_KEY',
          url: 'https://example.com/react-dashboard',
          type: 'image',
          format: 'png',
          width: 1920,
          height: 1080,
          waitUntil: 'networkIdle',
          cache_ttl: 0  // Disable caching for real-time updates
        },
        responseType: 'arraybuffer'
      });

      fs.writeFileSync(`react-dashboard-${Date.now()}.png`, updateResponse.data);
      console.log('Updated dashboard captured');
    }, 60000); // Capture every minute

  } catch (error) {
    console.error('Error capturing dashboard:', error);
  }
}

captureReactDashboard();

This approach:

  1. Captures the initial dashboard state using networkIdle
  2. Optionally sets up periodic captures for a dashboard with continuous updates
  3. Disables caching for the update captures to ensure fresh content

Conclusion: Mastering Dynamic Content Screenshots

Capturing accurate screenshots of dynamic content doesn't have to be a frustrating experience. With Scrnify's waitUntil parameter, you have precise control over when your screenshots are taken.

To recap the key points:

  • Use firstMeaningfulPaint (the default) for basic websites
  • Use load for static sites with many resources
  • Use networkIdle for SPAs and AJAX-heavy applications
  • Use networkAlmostIdle for sites with background connections
  • Consider advanced techniques for especially challenging content

By choosing the right waitUntil option for your specific use case, you can ensure your screenshots accurately capture dynamic content, even in complex modern web applications.

Get free access to the SCRNIFY API during our open beta and start generating screenshots today! scrnify.com

Have you encountered particularly challenging dynamic content scenarios? What strategies have worked for you? We'd love to hear about your experiences in the comments!

Cheers, Laura & Heidi 🇦🇹

P.S. If you're working with particularly complex SPAs, check out our upcoming article on integrating Scrnify with Puppeteer and Playwright for even more control over your screenshots!

Ready to Get Started?

Sign up now and start capturing stunning screenshots in minutes.

Sign Up Now