Handling Dynamic Content in Screenshots: SPAs, Lazy Loading, and Animations
Master the challenges of screenshotting dynamic web content. Wait strategies, scroll handling, and animation control for perfect captures.
Modern websites are dynamic—content loads asynchronously, images lazy load, and animations run continuously. Capturing accurate screenshots of these pages requires understanding the timing and using the right techniques.
This guide covers strategies for handling every type of dynamic content.
The Challenge
Traditional screenshots capture what's visible at a moment in time. But modern pages:
- Load content asynchronously (SPAs, AJAX)
- Lazy load images (scroll-triggered loading)
- Animate elements (transitions, motion)
- Infinite scroll (content loads as you scroll)
- Interactive states (hover effects, modals)
Capturing before content loads produces incomplete screenshots. Capturing during animations produces blurry or partially rendered content.
Wait Strategies
Basic Delay
The simplest approach—wait a fixed time:
{
url: 'https://example.com',
delay: 2000, // Wait 2 seconds
}
Pros: Simple, works for most pages Cons: May under-wait (incomplete) or over-wait (slow)
Wait for Selector
Wait for a specific element to appear:
{
url: 'https://example.com',
waitFor: '.main-content', // CSS selector
}
Pros: Precise, adapts to page speed Cons: Need to know what to wait for
Wait for Network Idle
Wait until no network requests for a period:
{
url: 'https://example.com',
waitUntil: 'networkidle0', // No requests for 500ms
}
Options:
networkidle0: Zero requests for 500msnetworkidle2: At most 2 requests for 500msdomcontentloaded: DOM is readyload: All resources loaded
Combined Approach
Use multiple conditions for reliability:
{
url: 'https://example.com',
waitUntil: 'networkidle2',
waitFor: '[data-loaded="true"]', // App-specific indicator
delay: 500, // Additional buffer
}
Single Page Applications (SPAs)
SPAs render content client-side, often after initial page load.
React/Vue/Angular Apps
Problem: Initial HTML is minimal; content renders via JavaScript.
Solution:
{
url: 'https://spa-example.com/dashboard',
waitUntil: 'networkidle0', // Wait for API calls
waitFor: '.dashboard-loaded', // Wait for render
delay: 1000, // Buffer for hydration
}
React Query/SWR Loading States
Many apps show loading states before data:
{
url: 'https://app.example.com',
waitFor: ':not(.loading-spinner)', // Wait for spinner to disappear
// Or wait for actual content
waitFor: '[data-testid="content-loaded"]',
}
Router Navigation
For SPAs with client-side routing:
// The URL hash/path may not trigger navigation
// Use inject script to ensure route loads
{
url: 'https://spa.example.com',
injectScripts: [`
// Wait for React Router or Vue Router
await new Promise(r => setTimeout(r, 2000));
// Or trigger navigation
window.history.pushState({}, '', '/dashboard');
await new Promise(r => setTimeout(r, 1000));
`],
}
Lazy Loading Images
Images that load only when scrolled into view.
Full Page Capture
Full-page mode automatically scrolls, triggering lazy load:
{
url: 'https://example.com',
fullPage: true,
// Images load as page scrolls
}
Viewport-Only Capture
For viewport captures, scroll then capture:
{
url: 'https://example.com',
injectScripts: [`
// Scroll to trigger lazy loads
window.scrollTo(0, document.body.scrollHeight);
await new Promise(r => setTimeout(r, 500));
window.scrollTo(0, 0);
await new Promise(r => setTimeout(r, 500));
`],
delay: 1000,
}
Disable Lazy Loading
Force immediate image loading:
{
url: 'https://example.com',
injectScripts: [`
// Remove lazy loading attributes
document.querySelectorAll('img[loading="lazy"]').forEach(img => {
img.loading = 'eager';
img.src = img.dataset.src || img.src;
});
await new Promise(r => setTimeout(r, 2000));
`],
}
Handling Animations
Animations can result in blurry or mid-transition captures.
Disable All Animations
{
url: 'https://example.com',
injectStyles: `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
`,
}
Wait for Animation Completion
If animations should complete before capture:
{
url: 'https://example.com',
delay: 3000, // Wait for animations
injectScripts: [`
// Wait for specific animation
document.querySelector('.hero-animation')
.addEventListener('animationend', resolve);
`],
}
Pause Video/GIF Content
{
url: 'https://example.com',
injectScripts: [`
// Pause all videos
document.querySelectorAll('video').forEach(v => v.pause());
// Stop GIFs by replacing with static
document.querySelectorAll('img[src*=".gif"]').forEach(img => {
// Replace with first frame or placeholder
});
`],
}
Infinite Scroll
Pages that load content as you scroll.
Capture First N Items
{
url: 'https://feed.example.com',
viewport: { height: 2000 }, // Taller viewport
injectScripts: [`
// Scroll to load more
for (let i = 0; i < 3; i++) {
window.scrollTo(0, document.body.scrollHeight);
await new Promise(r => setTimeout(r, 1000));
}
window.scrollTo(0, 0);
`],
}
Full Page with Limit
{
url: 'https://feed.example.com',
fullPage: true,
maxHeight: 10000, // Cap at 10000px to avoid infinite scroll
}
Hover States
Capture elements in hover state:
{
url: 'https://example.com',
injectScripts: [`
// Trigger hover state
const button = document.querySelector('.cta-button');
button.classList.add('hover');
// Or dispatch event
button.dispatchEvent(new MouseEvent('mouseenter'));
await new Promise(r => setTimeout(r, 500));
`],
}
Modal and Overlay Content
Capture with Modal Open
{
url: 'https://example.com',
injectScripts: [`
// Click to open modal
document.querySelector('.open-modal-btn').click();
await new Promise(r => setTimeout(r, 500));
`],
}
Dismiss Overlays Before Capture
{
url: 'https://example.com',
injectScripts: [`
// Close cookie banner
document.querySelector('.cookie-accept')?.click();
// Close newsletter popup
document.querySelector('.popup-close')?.click();
await new Promise(r => setTimeout(r, 500));
`],
// Or use AI removal
aiRemoval: {
enabled: true,
types: ['cookie-banner', 'popup', 'modal'],
},
}
Charts and Data Visualizations
Charts often render asynchronously.
D3/Chart.js/Highcharts
{
url: 'https://dashboard.example.com',
waitFor: 'svg.chart-rendered', // Wait for chart SVG
delay: 2000, // Extra time for transitions
}
Canvas-Based Charts
{
url: 'https://dashboard.example.com',
injectScripts: [`
// Wait for chart instance
await new Promise(resolve => {
const check = () => {
if (window.myChart?.rendered) resolve();
else setTimeout(check, 100);
};
check();
});
`],
}
Maps
Google Maps/Mapbox
{
url: 'https://map.example.com',
delay: 3000, // Wait for tiles to load
injectScripts: [`
// Wait for map ready
await new Promise(resolve => {
google.maps.event.addListenerOnce(map, 'idle', resolve);
});
`],
}
Best Practices
1. Test Your Selectors
Before production, test wait conditions:
// Test in browser console
document.querySelector('.your-selector') !== null
2. Use Data Attributes
Add data attributes specifically for screenshot detection:
<div data-screenshot-ready="true">
<!-- Content -->
</div>
waitFor: '[data-screenshot-ready="true"]'
3. Combine Strategies
Layer multiple wait conditions:
{
waitUntil: 'networkidle2',
waitFor: '.content-loaded',
delay: 500,
}
4. Handle Failures
Content may not load; have fallbacks:
{
waitFor: '.optional-content',
waitForTimeout: 5000, // Max wait before proceeding
}
5. Environment Differences
Development may load faster than production:
const delay = process.env.NODE_ENV === 'production' ? 3000 : 1000;
Debugging Tips
1. Increase Delay Dramatically
Start with a very long delay to see if content ever loads:
delay: 10000 // 10 seconds
2. Capture Intermediate States
Take multiple screenshots to identify when content appears:
// Capture at different times
for (const delay of [0, 1000, 2000, 3000]) {
await capture({ delay, filename: `debug-${delay}ms.png` });
}
3. Check Console Errors
Some content may fail to load due to errors:
injectScripts: [`
window.onerror = (msg) => console.log('Error:', msg);
`]
Conclusion
Dynamic content requires thoughtful wait strategies:
- Know your content - Understand how the page loads
- Wait appropriately - Combine delay, selector, and network waits
- Handle edge cases - Animations, lazy loading, infinite scroll
- Test thoroughly - Verify captures match expectations
With proper configuration, you can capture any dynamic page accurately.
Ready to capture dynamic content?
Get your free API key → - 100 free screenshots to get started.
See also: Visual Regression Testing Guide →
About the Author

Asad Ali
Full-Stack Developer and Founder of ZTabs with 8+ years of experience building scalable web applications and APIs. Specializes in performance optimization, SaaS development, and modern web technologies.
Credentials: Founder & CEO at ZTabs, Full-Stack Developer, Expert in Next.js, React, Node.js, and API optimization
Ready to capture your first screenshot?
Get started with 100 free screenshots. No credit card required.