Dynamic Content Screenshots: Capturing JavaScript-Heavy Pages
Master the challenges of capturing screenshots from dynamic, JavaScript-heavy web applications with advanced timing and synchronization techniques.
Table of Contents
Table of Contents
Dynamic Content Screenshots: Capturing JavaScript-Heavy Pages
Capturing screenshots from dynamic, JavaScript-heavy applications presents unique challenges including timing, synchronization, and content loading. Mastering these challenges enables reliable capture of modern web applications.
Dynamic Content Screenshots Overview
Overview {#overview}
Modern web applications rely heavily on JavaScript for rendering, creating challenges for screenshot capture. SPAs (Single Page Applications), lazy-loaded content, and animations require sophisticated timing and synchronization strategies.
Key Challenges:
- Asynchronous Loading: Content loads after initial page load
- JS Frameworks: React, Vue, Angular render dynamically
- Animations: CSS/JS animations must complete before capture
- API Calls: Data fetched asynchronously after page load
- Lazy Loading: Images and content load on scroll/interaction
Rendering Challenges {#rendering-challenges}
Understanding rendering lifecycle is critical for timing screenshots correctly.
Page Lifecycle
interface PageLifecycle {
domContentLoaded: number
loaded: number
networkIdle: number
firstContentfulPaint: number
largestContentfulPaint: number
allAnimationsComplete: number
}
class LifecycleTracker {
async trackPageLifecycle(page: any): Promise<PageLifecycle> {
const timings: Partial<PageLifecycle> = {}
// Wait for DOMContentLoaded
await page.waitForEvent('domcontentloaded')
timings.domContentLoaded = Date.now()
// Wait for window.load
await page.waitForEvent('load')
timings.loaded = Date.now()
// Wait for network idle
await page.waitForLoadState('networkidle')
timings.networkIdle = Date.now()
// Measure rendering metrics
const metrics = await page.evaluate(() => {
const paint = performance.getEntriesByType('paint')
const fcp = paint.find(e => e.name === 'first-contentful-paint')
const lcp = paint.find(e => e.name === 'largest-contentful-paint')
return {
fcp: fcp?.startTime || 0,
lcp: lcp?.startTime || 0
}
})
timings.firstContentfulPaint = metrics.fcp
timings.largestContentfulPaint = metrics.lcp
return timings as PageLifecycle
}
}JavaScript Execution {#javascript-execution}
Properly executing JavaScript ensures content is fully rendered.
Wait for JS Frameworks
interface FrameworkDetection {
framework: 'react' | 'vue' | 'angular' | 'svelte' | 'vanilla'
detected: boolean
readySelector?: string
customWait?: (page: any) => Promise<void>
}
class FrameworkWaiter {
async waitForFramework(page: any, url: string): Promise<FrameworkDetection> {
// Detect framework
const framework = await page.evaluate(() => {
if (window.React) return 'react'
if (window.Vue) return 'vue'
if (window.ng) return 'angular'
return 'vanilla'
})
// Framework-specific wait strategies
switch (framework) {
case 'react':
await this.waitForReact(page)
break
case 'vue':
await this.waitForVue(page)
break
case 'angular':
await this.waitForAngular(page)
break
default:
await page.waitForLoadState('networkidle')
}
return {
framework,
detected: framework !== 'vanilla',
readySelector: this.getReadySelector(framework)
}
}
private async waitForReact(page: any): Promise<void> {
// Wait for React to finish rendering
await page.evaluate(() => {
return new Promise(resolve => {
// Wait for React root to be mounted
const checkReact = () => {
const root = document.querySelector('[data-reactroot], #root, #app')
if (root && root.children.length > 0) {
// Wait one more tick for effects
setTimeout(resolve, 100)
} else {
setTimeout(checkReact, 50)
}
}
checkReact()
})
})
}
private async waitForVue(page: any): Promise<void> {
// Wait for Vue to finish rendering
await page.evaluate(() => {
return new Promise(resolve => {
if (window.Vue) {
// Wait for next tick after Vue mounts
window.Vue.nextTick(() => {
setTimeout(resolve, 100)
})
} else {
setTimeout(resolve, 500)
}
})
})
}
private async waitForAngular(page: any): Promise<void> {
// Wait for Angular to stabilize
await page.evaluate(() => {
return new Promise(resolve => {
const checkAngular = () => {
// @ts-ignore
if (window.getAllAngularTestabilities) {
// @ts-ignore
const testabilities = window.getAllAngularTestabilities()
const allStable = testabilities.every((t: any) => t.isStable())
if (allStable) {
setTimeout(resolve, 100)
} else {
setTimeout(checkAngular, 50)
}
} else {
setTimeout(resolve, 500)
}
}
checkAngular()
})
})
}
private getReadySelector(framework: string): string {
const selectors: Record<string, string> = {
react: '[data-reactroot], #root',
vue: '[data-v-app], #app',
angular: '[ng-version]',
vanilla: 'body'
}
return selectors[framework] || 'body'
}
}Wait Strategies {#wait-strategies}
Different wait strategies for different content types.
Smart Waiting
interface WaitStrategy {
name: string
condition: (page: any) => Promise<boolean>
timeout: number
fallback?: string
}
class SmartWaiter {
private strategies: WaitStrategy[] = [
{
name: 'network_idle',
condition: async (page) => {
await page.waitForLoadState('networkidle', { timeout: 5000 })
return true
},
timeout: 5000
},
{
name: 'dom_stable',
condition: async (page) => {
return await page.evaluate(() => {
return new Promise(resolve => {
let mutations = 0
const observer = new MutationObserver(() => mutations++)
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
})
setTimeout(() => {
observer.disconnect()
resolve(mutations < 5) // Less than 5 mutations = stable
}, 1000)
})
})
},
timeout: 2000
},
{
name: 'images_loaded',
condition: async (page) => {
return await page.evaluate(() => {
const images = Array.from(document.images)
return images.every(img => img.complete)
})
},
timeout: 3000
},
{
name: 'animations_complete',
condition: async (page) => {
return await page.evaluate(() => {
return document.getAnimations().length === 0
})
},
timeout: 2000
}
]
async waitForReady(page: any, options: {
strategies?: string[]
maxWait?: number
} = {}): Promise<{
ready: boolean
strategiesUsed: string[]
totalWaitTime: number
}> {
const startTime = Date.now()
const maxWait = options.maxWait || 10000
const strategiesToUse = options.strategies || ['network_idle', 'dom_stable', 'images_loaded']
const strategiesUsed: string[] = []
for (const strategyName of strategiesToUse) {
const strategy = this.strategies.find(s => s.name === strategyName)
if (!strategy) continue
try {
const ready = await strategy.condition(page)
strategiesUsed.push(strategyName)
if (!ready && Date.now() - startTime < maxWait) {
// Wait a bit longer
await page.waitForTimeout(500)
}
} catch (error) {
console.warn(`Strategy ${strategyName} failed:`, error)
}
}
return {
ready: true,
strategiesUsed,
totalWaitTime: Date.now() - startTime
}
}
}Handling Dynamic Elements {#dynamic-elements}
Special handling for common dynamic patterns.
Lazy Loading
class LazyLoadHandler {
async triggerLazyLoad(page: any): Promise<void> {
// Scroll to trigger lazy-loaded images
await page.evaluate(async () => {
const scrollHeight = document.documentElement.scrollHeight
const viewportHeight = window.innerHeight
for (let y = 0; y < scrollHeight; y += viewportHeight) {
window.scrollTo(0, y)
await new Promise(resolve => setTimeout(resolve, 200))
}
// Scroll back to top
window.scrollTo(0, 0)
await new Promise(resolve => setTimeout(resolve, 100))
})
// Wait for images to load
await page.evaluate(() => {
return new Promise(resolve => {
const checkImages = () => {
const images = Array.from(document.images)
const allLoaded = images.every(img => img.complete)
if (allLoaded) {
resolve(true)
} else {
setTimeout(checkImages, 100)
}
}
checkImages()
})
})
}
}Infinite Scroll
class InfiniteScrollHandler {
async loadInfiniteScroll(page: any, maxScrolls: number = 5): Promise<number> {
let scrollCount = 0
let previousHeight = 0
while (scrollCount < maxScrolls) {
const currentHeight = await page.evaluate(() => document.documentElement.scrollHeight)
if (currentHeight === previousHeight) {
// No more content loaded
break
}
// Scroll to bottom
await page.evaluate(() => {
window.scrollTo(0, document.documentElement.scrollHeight)
})
// Wait for new content
await page.waitForTimeout(1000)
previousHeight = currentHeight
scrollCount++
}
// Scroll back to top
await page.evaluate(() => window.scrollTo(0, 0))
await page.waitForTimeout(200)
return scrollCount
}
}Implementation {#implementation}
Complete implementation for dynamic content screenshots.
import { chromium, Page } from 'playwright'
class DynamicScreenshotService {
private frameworkWaiter: FrameworkWaiter
private smartWaiter: SmartWaiter
private lazyLoadHandler: LazyLoadHandler
constructor() {
this.frameworkWaiter = new FrameworkWaiter()
this.smartWaiter = new SmartWaiter()
this.lazyLoadHandler = new LazyLoadHandler()
}
async captureScreenshot(url: string, options: {
waitForFramework?: boolean
triggerLazyLoad?: boolean
viewport?: { width: number; height: number }
fullPage?: boolean
} = {}): Promise<{
screenshot: Buffer
metadata: {
renderTime: number
framework?: string
strategiesUsed: string[]
}
}> {
const browser = await chromium.launch()
const page = await browser.newPage({
viewport: options.viewport || { width: 1920, height: 1080 }
})
const startTime = Date.now()
try {
// Navigate to page
await page.goto(url, { waitUntil: 'domcontentloaded' })
// Wait for framework if requested
let framework
if (options.waitForFramework) {
framework = await this.frameworkWaiter.waitForFramework(page, url)
}
// Smart waiting
const waitResult = await this.smartWaiter.waitForReady(page, {
strategies: ['network_idle', 'dom_stable', 'images_loaded']
})
// Trigger lazy load if requested
if (options.triggerLazyLoad) {
await this.lazyLoadHandler.triggerLazyLoad(page)
}
// Final wait for animations
await page.waitForTimeout(500)
// Capture screenshot
const screenshot = await page.screenshot({
fullPage: options.fullPage || false,
type: 'png'
})
const renderTime = Date.now() - startTime
return {
screenshot,
metadata: {
renderTime,
framework: framework?.framework,
strategiesUsed: waitResult.strategiesUsed
}
}
} finally {
await browser.close()
}
}
}Advanced Timing Control
class AdvancedTiming {
async waitForCustomCondition(
page: any,
condition: string,
timeout: number = 5000
): Promise<boolean> {
try {
await page.waitForFunction(condition, { timeout })
return true
} catch (error) {
console.warn('Custom condition timeout:', condition)
return false
}
}
async waitForSelector(
page: any,
selector: string,
options: { timeout?: number; visible?: boolean } = {}
): Promise<boolean> {
try {
await page.waitForSelector(selector, {
timeout: options.timeout || 5000,
state: options.visible ? 'visible' : 'attached'
})
return true
} catch (error) {
console.warn(`Selector not found: ${selector}`)
return false
}
}
async waitForAPIResponse(
page: any,
urlPattern: string,
timeout: number = 5000
): Promise<boolean> {
try {
await page.waitForResponse(
(response: any) => response.url().includes(urlPattern),
{ timeout }
)
return true
} catch (error) {
console.warn(`API response timeout: ${urlPattern}`)
return false
}
}
}Conclusion {#conclusion}
Capturing dynamic content requires understanding JavaScript frameworks, implementing smart wait strategies, handling lazy loading, and managing animations. Success depends on combining multiple detection methods, using framework-specific waits, and implementing fallback strategies.
Key success factors include detecting and waiting for specific frameworks, using network idle and DOM stability checks, triggering lazy load before capture, and implementing timeout protection to prevent indefinite waits.
Capture modern web applications reliably with our screenshot APIs, designed to handle dynamic content, JavaScript frameworks, and complex rendering scenarios.