Table of contents

How do I scrape data from websites that use lazy loading?

Lazy loading is a web optimization technique where content is loaded dynamically as users scroll down the page or interact with specific elements. This approach improves initial page load times but presents unique challenges for web scrapers. When scraping lazy-loaded websites, you need to trigger the loading mechanisms and wait for content to appear before extracting data.

Understanding Lazy Loading Mechanisms

Lazy loading typically works through several mechanisms:

  • Scroll-based loading: Content loads when users scroll to specific page positions
  • Intersection Observer API: Modern browsers detect when elements enter the viewport
  • Click-based loading: "Load More" buttons trigger additional content
  • Time-based delays: Content appears after predetermined intervals
  • AJAX requests: Background requests fetch new data without page refreshes

Scraping Lazy-Loaded Content with Puppeteer

Puppeteer excels at handling lazy-loaded content because it controls a real Chrome browser instance. Here's how to scrape different types of lazy loading:

Basic Scroll-Based Lazy Loading

const puppeteer = require('puppeteer');

async function scrapeLazyContent() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.goto('https://example.com/lazy-loading-page');

  // Wait for initial content to load
  await page.waitForSelector('.content-container');

  let previousHeight = 0;
  let currentHeight = await page.evaluate('document.body.scrollHeight');

  // Keep scrolling until no new content loads
  while (previousHeight !== currentHeight) {
    previousHeight = currentHeight;

    // Scroll to bottom
    await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');

    // Wait for new content to load
    await page.waitForTimeout(2000);

    // Check if page height increased
    currentHeight = await page.evaluate('document.body.scrollHeight');
  }

  // Extract all loaded content
  const items = await page.$$eval('.lazy-item', elements => 
    elements.map(el => ({
      title: el.querySelector('.title')?.textContent,
      description: el.querySelector('.description')?.textContent,
      image: el.querySelector('img')?.src
    }))
  );

  await browser.close();
  return items;
}

Advanced Lazy Loading with Network Monitoring

For more sophisticated lazy loading detection, monitor network requests to know when new content finishes loading:

async function scrapeWithNetworkMonitoring() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // Track network requests
  let pendingRequests = 0;

  page.on('request', () => pendingRequests++);
  page.on('response', () => pendingRequests--);

  await page.goto('https://example.com/infinite-scroll');

  async function waitForNetworkIdle() {
    return new Promise(resolve => {
      const check = () => {
        if (pendingRequests === 0) {
          resolve();
        } else {
          setTimeout(check, 100);
        }
      };
      check();
    });
  }

  // Scroll and wait for network activity to complete
  for (let i = 0; i < 10; i++) {
    await page.evaluate('window.scrollBy(0, window.innerHeight)');
    await waitForNetworkIdle();
    await page.waitForTimeout(1000);
  }

  const content = await page.content();
  await browser.close();
  return content;
}

Using Playwright for Lazy Loading

Playwright offers similar capabilities with some additional features for handling lazy loading:

const { chromium } = require('playwright');

async function scrapeLazyPlaywright() {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto('https://example.com/lazy-content');

  // Use Playwright's built-in network idle waiting
  await page.waitForLoadState('networkidle');

  // Scroll incrementally and wait for content
  let hasMoreContent = true;
  while (hasMoreContent) {
    const itemCountBefore = await page.locator('.lazy-item').count();

    // Scroll down
    await page.evaluate(() => {
      window.scrollBy(0, window.innerHeight);
    });

    // Wait for potential new content
    await page.waitForTimeout(2000);
    await page.waitForLoadState('networkidle');

    const itemCountAfter = await page.locator('.lazy-item').count();
    hasMoreContent = itemCountAfter > itemCountBefore;
  }

  // Extract all loaded items
  const items = await page.locator('.lazy-item').all();
  const data = [];

  for (const item of items) {
    data.push({
      title: await item.locator('.title').textContent(),
      url: await item.locator('a').getAttribute('href')
    });
  }

  await browser.close();
  return data;
}

Handling Different Lazy Loading Patterns

Load More Buttons

Many sites use "Load More" buttons instead of infinite scroll:

async function scrapeLoadMoreButton() {
  const page = await browser.newPage();
  await page.goto('https://example.com/load-more-content');

  // Keep clicking "Load More" until it disappears
  while (true) {
    try {
      const loadMoreBtn = await page.waitForSelector(
        '.load-more-btn', 
        { timeout: 3000 }
      );

      if (!loadMoreBtn) break;

      await loadMoreBtn.click();

      // Wait for new content to load
      await page.waitForFunction(
        (selector) => document.querySelectorAll(selector).length > 0,
        {},
        '.new-content-indicator'
      );

    } catch (error) {
      // No more "Load More" button found
      break;
    }
  }

  return await page.$$eval('.content-item', items => 
    items.map(item => item.textContent)
  );
}

Image Lazy Loading

For lazy-loaded images, you need to ensure images are fully loaded:

async function scrapeLazyImages() {
  const page = await browser.newPage();
  await page.goto('https://example.com/image-gallery');

  // Scroll to load all images
  await autoScroll(page);

  // Wait for all images to load
  await page.evaluate(() => {
    const images = Array.from(document.querySelectorAll('img'));
    return Promise.all(
      images.map(img => {
        if (img.complete) return Promise.resolve();
        return new Promise(resolve => {
          img.onload = resolve;
          img.onerror = resolve;
        });
      })
    );
  });

  // Extract image data
  const imageData = await page.$$eval('img', images =>
    images.map(img => ({
      src: img.src,
      alt: img.alt,
      width: img.naturalWidth,
      height: img.naturalHeight
    }))
  );

  return imageData;
}

Python Solutions with Selenium

For Python developers, Selenium provides similar capabilities:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

def scrape_lazy_loading_selenium():
    driver = webdriver.Chrome()
    driver.get('https://example.com/lazy-content')

    # Get initial page height
    last_height = driver.execute_script("return document.body.scrollHeight")

    while True:
        # Scroll to bottom
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

        # Wait for new content to load
        time.sleep(2)

        # Calculate new scroll height
        new_height = driver.execute_script("return document.body.scrollHeight")

        if new_height == last_height:
            break
        last_height = new_height

    # Extract data after all content is loaded
    items = driver.find_elements(By.CLASS_NAME, "lazy-item")
    data = []

    for item in items:
        try:
            title = item.find_element(By.CLASS_NAME, "title").text
            description = item.find_element(By.CLASS_NAME, "description").text
            data.append({"title": title, "description": description})
        except:
            continue

    driver.quit()
    return data

Best Practices for Lazy Loading Scraping

1. Implement Robust Wait Strategies

Always use multiple wait conditions to ensure content loads completely:

async function robustWaitStrategy(page, selector) {
  // Wait for element to exist
  await page.waitForSelector(selector);

  // Wait for network to be idle
  await page.waitForLoadState('networkidle');

  // Wait for any animations to complete
  await page.waitForTimeout(1000);

  // Verify content is actually visible
  await page.waitForFunction(
    sel => {
      const el = document.querySelector(sel);
      return el && el.offsetHeight > 0;
    },
    selector
  );
}

2. Handle Rate Limiting

Implement delays and respect website performance:

async function respectfulScraping(page) {
  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

  for (let i = 0; i < 10; i++) {
    await page.evaluate('window.scrollBy(0, 500)');
    await delay(Math.random() * 2000 + 1000); // Random delay 1-3 seconds

    // Check if we should continue
    const hasMoreContent = await page.evaluate(() => {
      return window.innerHeight + window.scrollY < document.body.offsetHeight;
    });

    if (!hasMoreContent) break;
  }
}

3. Error Handling and Retries

Implement robust error handling for unreliable lazy loading:

async function scrapeWithRetries(url, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const browser = await puppeteer.launch();
      const page = await browser.newPage();

      await page.goto(url, { waitUntil: 'networkidle2' });

      // Your scraping logic here
      const result = await performScraping(page);

      await browser.close();
      return result;

    } catch (error) {
      console.log(`Attempt ${attempt} failed:`, error.message);

      if (attempt === maxRetries) {
        throw new Error(`Failed after ${maxRetries} attempts`);
      }

      // Wait before retry
      await new Promise(resolve => setTimeout(resolve, 2000 * attempt));
    }
  }
}

Advanced Techniques

Intersection Observer Detection

Some sites use modern Intersection Observer API. You can simulate this behavior:

async function triggerIntersectionObserver(page) {
  await page.evaluateOnNewDocument(() => {
    // Override Intersection Observer to trigger immediately
    const OriginalIntersectionObserver = window.IntersectionObserver;
    window.IntersectionObserver = class {
      constructor(callback, options) {
        this.callback = callback;
        this.options = options;
      }

      observe(element) {
        // Immediately trigger callback
        this.callback([{
          isIntersecting: true,
          target: element
        }]);
      }

      unobserve() {}
      disconnect() {}
    };
  });
}

Using WebScraping.AI API

For simpler lazy loading scenarios, you can use the WebScraping.AI API with JavaScript execution:

curl -X POST "https://api.webscraping.ai/html" \
  -H "Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/lazy-content",
    "js": true,
    "js_timeout": 10000,
    "js_script": "window.scrollTo(0, document.body.scrollHeight); await new Promise(resolve => setTimeout(resolve, 3000));"
  }'

Or using JavaScript:

const response = await fetch('https://api.webscraping.ai/html', {
  method: 'POST',
  headers: {
    'Api-Key': 'your-api-key',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://example.com/lazy-content',
    js: true,
    js_timeout: 10000,
    js_script: `
      // Scroll to trigger lazy loading
      window.scrollTo(0, document.body.scrollHeight);

      // Wait for content to load
      await new Promise(resolve => setTimeout(resolve, 3000));
    `
  })
});

const html = await response.text();

Troubleshooting Common Issues

Content Not Loading

If lazy-loaded content isn't appearing, try: - Increasing wait timeouts - Verifying scroll triggers work correctly - Checking if content requires user interaction beyond scrolling - Using how to handle timeouts in Puppeteer for better timeout management

Incomplete Data Extraction

Ensure all network requests complete before data extraction: - Monitor network activity using browser dev tools - Implement proper network idle waiting - Use multiple verification methods to confirm content loads

Memory and Performance Issues

For large-scale lazy loading scraping: - Close browser instances properly - Implement pagination to avoid memory overflow - Use headless mode for better performance - Consider how to handle AJAX requests using Puppeteer for dynamic content handling

Anti-Bot Detection

To avoid detection while scraping lazy-loaded content: - Use realistic scroll speeds and patterns - Implement random delays between actions - Rotate user agents and browser fingerprints - Respect robots.txt and rate limiting policies

Conclusion

Scraping lazy-loaded websites requires patience, robust waiting strategies, and proper understanding of how the content loading mechanisms work. The key is to trigger the loading events correctly and wait for content to fully load before attempting data extraction. Whether using Puppeteer, Playwright, or Selenium, always implement proper error handling and respect website performance limitations.

For complex scenarios involving authentication during lazy loading scraping, consider reading about handling authentication in Puppeteer to maintain sessions while triggering lazy loading mechanisms.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon