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:
- Your screenshot tool loads the URL
- The initial HTML loads quickly
- The tool takes a screenshot immediately
- JavaScript runs and AJAX requests complete after the screenshot is taken
- 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 thanload
but may miss content from external resources.load
: This fires when all resources (images, stylesheets, etc.) have finished loading. It's more comprehensive thanDOMContentLoaded
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:
- Make a request to the dashboard URL
- Wait for all network activity to complete (including AJAX requests)
- Capture the fully loaded dashboard
- 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:
-
Try different waitUntil options: Start with
networkIdle
and work backward to identify which stage is missing your content. -
Inspect network activity: Use browser DevTools to observe when your content actually loads. Look for delayed API calls or websocket connections.
-
Check for lazy loading: Some content only loads when scrolled into view. Try using
fullPage=true
to trigger this content. -
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.
-
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:
- Initial HTML loads (fast)
- React framework bootstraps (medium)
- API calls fetch dashboard data (slow)
- 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:
- Captures the initial dashboard state using
networkIdle
- Optionally sets up periodic captures for a dashboard with continuous updates
- 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!