How to Scrape Dynamic Web Pages
To scrape dynamic content, find where the data really comes from: a JSON API the page calls, JSON embedded in the HTML, or only the rendered page. Call the API or parse the embedded JSON when you can, and run a headless browser (Playwright, Puppeteer or Selenium) only for the pages that need one, waiting for the element you want rather than a fixed time; a browser downloads every script, image and font, so block what you don't need and budget more traffic per page than for plain HTML. ProxyField residential proxies work with every headless browser from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.
- residential IPs
- 412M+
- from, traffic never expires
- $1.95/GB
- Easy Trial
- 1 GB for $2.95
On this page 10 sections
- What dynamic content is
- How to tell if a page needs JavaScript rendering
- Why your scraper returns empty results
- Method 1: call the API behind the page
- Method 2: read the JSON embedded in the HTML
- Method 3: render the page in a headless browser
- How to crawl a JavaScript website
- Challenges of scraping dynamic content at scale
- Proxies and traffic for dynamic pages
- How to Scrape Dynamic Web Pages FAQ
What dynamic content is
A static page arrives from the server with its content already in the HTML. A dynamic page arrives as a shell, and JavaScript in the browser fills it in: it calls an API, receives JSON and builds the list, the prices or the comments on the spot. Single-page apps work this way, and so do many ordinary sites for parts of a page:
- product grids, search results and filters that update without a reload;
- infinite scroll and "Load more" buttons;
- prices, stock or reviews loaded after the main page;
- content that appears only after a click, a hover or a cookie banner.
| Feature | Static page | Dynamic page |
|---|---|---|
| Where the data is | In the HTML the server sends | Added by JavaScript after load |
| What a plain HTTP request gets | Everything | An empty template or part of the page |
| Fastest way to scrape | HTTP client plus a parser | The API the page calls, or embedded JSON |
| When you need a browser | Never | Only when there is no usable API or JSON |
How to tell if a page needs JavaScript rendering
Four checks, each under a minute:
-
View the source
Ctrl+U (Cmd+Option+U on a Mac) shows the HTML the server sent. Search for a value you can see on the page. Missing means it is added later.
-
Fetch it without a browser
Curl -s https://example.com/page | grep "Blue Widget": no match means the same thing, from your scraper's point of view. -
Turn JavaScript off
In Chrome DevTools, open the command menu (Ctrl+Shift+P), run "Disable JavaScript" and reload: what disappears is dynamic.
-
Watch the Network panel
Filter by Fetch/XHR and reload or scroll; the requests that return JSON with your data are the page's API.
Why your scraper returns empty results
An empty list, None from a selector or a page that looks like a skeleton almost always means one of these:
| Cause | How to recognize it | Fix |
|---|---|---|
| The data is loaded by JavaScript | The value is missing from View Source | Call the API or parse embedded JSON; render only if neither works |
| A cookie banner that hides the page | The HTML is a banner, not the page | Send the cookie the banner sets when accepted, or click it in a browser |
| A block or challenge page | Status 403 or 429, or a "checking your browser" page with status 200 | Slow down, send complete browser headers, change the IP type; see the Cloudflare 403 guide |
| A different page for your location or device | The browser and the scraper see different layouts | Send the same User-Agent and Accept-Language, and use an IP in the right country |
| The selector is wrong | The value is in the source, under another class | Copy the selector from the HTML your scraper received, not from DevTools after scripts ran |
Method 1: call the API behind the page
Most dynamic pages get their data from a JSON endpoint, and that endpoint is the cleanest thing to scrape: smaller than the page, already structured, and stable across redesigns more often than the HTML. In the Network panel, click the request that holds your data and look at:
- the URL and parameters:
page,offset,cursor,limit, filters; - the headers the endpoint needs: often
Accept,Referer, sometimes a token the page got from an earlier request; - GraphQL: a
POSTto/graphqlwith a query and variables in the body; replay the same body with your own variables.
Right-click and Copy as cURL to reproduce it, then move it into code. Call only the endpoints the public page itself uses, at a pace a visitor would.
Method 2: read the JSON embedded in the HTML
Frameworks often ship the page's data inside the HTML so the first render is fast. Look in the source for:
<script id="__NEXT_DATA__" type="application/json">(Next.js),window.__NUXT__(Nuxt) orwindow.__INITIAL_STATE__/__APOLLO_STATE__assignments,<script type="application/ld+json">with schema.org data such asProduct,OfferorJobPosting.
Extract the script's text, parse it as JSON, and you have the data without running a browser. For a Python walkthrough of Methods 1 and 2, see Python dynamic web scraping.
Method 3: render the page in a headless browser
When the data exists only after scripts run, use a browser. This Node.js example uses Playwright (npm playwright 1.64.1, Apache-2.0, 2026-10-23): it routes through a proxy, blocks heavy resources, waits for the product cards, and also captures the JSON the page fetches, which is often the better source.
// npm i playwright && npx playwright install chromium
// save as scrape.mjs (ES module, top-level await)
import { chromium } from 'playwright';
const browser = await chromium.launch({
proxy: { server: 'http://gate.proxyfield.io:7000',
username: '<login>-country-us-city-new_york', password: '<password>' },
});
const page = await browser.newPage();
await page.route('**/*', route =>
['image', 'media', 'font'].includes(route.request().resourceType())
? route.abort() : route.continue());
const apiResponse = page.waitForResponse(r => r.url().includes('/api/products') && r.ok())
.catch(() => null); // resolves to null if no such request
await page.goto('https://example.com/products', { waitUntil: 'domcontentloaded' });
await page.locator('div.product-card').first().waitFor();
const cards = await page.locator('div.product-card').evaluateAll(els =>
els.map(e => ({ name: e.querySelector('h2')?.innerText, price: e.querySelector('.price')?.innerText })));
const res = await apiResponse;
const json = res ? await res.json().catch(() => null) : null; // the data the page itself received
console.log(cards.length, json ? 'API captured' : 'no API response');
await browser.close();Patterns for the common cases:
- Wait for elements, not seconds.
locator.waitFor()orpage.waitForSelector(); a fixedwaitForTimeout(5000)is flaky and slow. - Infinite scroll: scroll (
page.mouse.wheel(0, 5000)), wait briefly, and stop when the item count stops growing; or call the API the scroll triggers. - "Load more" buttons and tabs: click until the button disappears, waiting for the new items after each click.
- Dynamic tables: read the table after it renders with
evaluateAll, or, better, take the JSON the table was built from.
The same patterns in Puppeteer and Selenium, with proxy setup: Puppeteer proxy setup, Playwright proxy setup, Selenium proxy setup. Headless browser trade-offs in depth: headless browser scraping.
How to crawl a JavaScript website
Crawling adds a second problem: the links themselves may be created by JavaScript, so an HTTP crawler finds no URLs to follow. In order of preference:
- Use the sitemap (
/sitemap.xml, or theSitemap:line in robots.txt): it lists the URLs without rendering anything. - Use the listing API to enumerate items, then build each detail URL.
- Render to discover links: a browser crawler such as Crawlee's
PlaywrightCrawlerrenders each page and enqueues the links it finds. Render the listing pages, and fetch detail pages over plain HTTP if they are static.
Search engines face the same problem: Google renders JavaScript pages in a second pass, after crawling the HTML, as described in Google Search Central: JavaScript SEO basics. Most other crawlers and simple scrapers do not run JavaScript at all.
Scrapy and JavaScript: Scrapy does not execute JavaScript. Its documentation recommends finding the data source first; for pages that truly need rendering, the scrapy-playwright plugin renders only the requests you mark with meta={"playwright": True} (Scrapy: selecting dynamically-loaded content).
Challenges of scraping dynamic content at scale
Rendering one page is easy; rendering a million is an infrastructure job.
| Challenge | Why it hurts | What helps |
|---|---|---|
| CPU and memory | Each browser page uses far more than an HTTP request | Render only pages that need it; reuse one browser with many contexts; cap pages per browser |
| Proxy traffic | A rendered page loads scripts, styles, images and fonts | Block images, media and fonts; capture the API JSON instead of the whole page |
| Timeouts and flakiness | Pages wait on third-party scripts and slow APIs | domcontentloaded plus a wait for your element; per-page timeouts; retries |
| Bot detection | Dynamic sites often run client-side checks | Realistic pacing and full browser headers; see how to avoid getting blocked while scraping |
| Session state | Multi-step flows need cookies and one IP | Sticky sessions for the flow; a new context per session |
| Changing front ends | Class names generated at build time change often | Prefer API JSON, data-* attributes, text and roles over generated classes |
Proxies and traffic for dynamic pages
Measure before you size a job: open DevTools' Network panel and compare the transferred size of one page with images, media and fonts allowed and blocked. Then:
- Rotation: per-request rotation for API calls and independent pages; a sticky session (30 seconds to 36 hours) when a browser loads a page and then makes its own API calls, so they all come from one visitor.
- Location: country, region and city targeting for prices and content that change by place. The ProxyField gateway is
gate.proxyfield.io:7000;<login>-country-us-city-new_yorkasks for an IP in New York. Region, session and session time go in the same username:-region-<region>,-session-<id>and-ttl-<seconds>(30 to 129,600 seconds, 20 minutes by default), as in<login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional. - Concurrency: ProxyField has unlimited concurrent threads, so the limit is the site's tolerance and your hardware; see how to make web scraping faster.
Residential proxies cost $2.95/GB for 1-9 GB down to $1.95/GB from 1,000 GB, with no subscription, and the traffic never expires: residential and mobile proxy pricing. If you would rather not run browsers, the ProxyField Web Scraping API fetches any URL at $2.00 per 1,000 successful results with JavaScript rendering, down to $1.00 at volume, or $0.50 with plain HTTP, down to $0.20, and you pay only for successful results; ask for it through Contact sales.
Scraping JavaScript-heavy sites? ProxyField residential proxies: 412M+ residential IPs in 195+ countries, per-request rotation or sticky sessions, from $1.95/GB. Start with the Easy Trial (1 GB); traffic never expires, and all purchases are final.
How to Scrape Dynamic Web Pages FAQ
How do I scrape dynamic web pages?
ProxyField residential proxies, from $1.95/GB, work with every headless browser, and you scrape dynamic web pages in 3 steps: call the JSON API the page uses (find it under Fetch/XHR in DevTools), otherwise parse the JSON embedded in the HTML, and only then render the page in a headless browser and wait for the element you need. Rendered pages use far more traffic than API calls, so block images, media and fonts.
What is the difference between static and dynamic websites for scraping?
A static page has its content in the HTML the server sends, so an HTTP client and a parser are enough. A dynamic page fills itself in with JavaScript after loading, so you either call the API it uses or render it in a browser.
How do I tell if a website needs JavaScript rendering to scrape?
Search the page source (Ctrl+U) or a curl response for a value you can see on the page: if it is missing, JavaScript adds it. Disabling JavaScript in DevTools and reloading shows the same thing, and the Network panel shows which requests bring the data.
Why does my scraper return empty results?
Usually because the data is loaded by JavaScript after the HTML arrives, so your selector finds an empty template. Other causes are a cookie banner that hides the page, a block page returned with status 200, a different layout for your location or device, or a selector copied from the rendered DOM instead of the raw HTML.
How do I scrape a single-page application?
A single-page app gets nearly all its data from API calls, so watch the Network panel while you click through it and call those endpoints directly. When an API needs tokens the app computes, drive the app in Playwright or Puppeteer and capture the JSON responses as they arrive.
How do I crawl a JavaScript website?
Start from the sitemap or the listing API to get URLs without rendering, and render only the pages whose links or content exist after JavaScript runs. Browser crawlers such as Crawlee's PlaywrightCrawler render each page and queue the links they find.
How do I execute JavaScript with Scrapy?
Scrapy itself does not run JavaScript; install the scrapy-playwright plugin and mark the requests that need a browser with meta={"playwright": True}. Scrapy's documentation suggests looking for the data's real source, such as an API or embedded JSON, before rendering.
What is JavaScript rendering in web scraping?
Running a page's scripts in a real browser engine, usually headless, so the scraper reads the page as a visitor sees it rather than the bare HTML. It is slower and heavier than a plain request, because the browser downloads and executes every script and asset.
What are the challenges of scraping dynamic content at scale?
CPU and memory per browser page, much more traffic per page, flaky timeouts, client-side bot checks, session state across steps, and front ends whose class names change with every build. Rendering only the pages that need it and capturing API JSON instead of the DOM solve most of them.
How do I scrape dynamic tables?
Find the request that fills the table: it usually returns JSON with every row, sometimes with a page size you can raise. If there is none, render the page, wait for the rows, read them with evaluateAll, and click through the table's pages.
Show 2 more questionsShow fewer questions
Can web crawlers execute JavaScript?
Some can: Google renders JavaScript pages in a second pass after crawling the HTML, and browser-based crawlers render every page. Most simple crawlers and HTTP scrapers do not run JavaScript, so they see only what the server sends.
Which programming languages can scrape JavaScript websites?
Any language that can drive a browser or call an API: Node.js and Python have Playwright and Puppeteer or Selenium, Java and C# have Playwright and Selenium bindings, and Ruby, Go and Rust have WebDriver or DevTools clients. For API-backed pages, a plain HTTP client in any language is enough.
Related pages
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.