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.

Dynamic Content Screenshots: Capturing JavaScript-Heavy Pages
August 18, 2025
12 min read
Website Screenshots

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

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.

Tags:dynamic-contentjavascript-appsspa-testingtiming-control