Scrape a JavaScript-rendered page once the app has loaded
To scrape a JavaScript-rendered page you need the DOM the browser builds, not the HTML the server sends. Single-page apps, dashboards, store fronts and job boards built with React, Vue or Angular ship an empty root element and fill it in later. The Scraping API renders the page in a real browser, waits for your element and then runs your rules.
The HTML of a React app has no data in it
Fetch a single-page app with an HTTP client and you get a script tag and an empty div. Every selector you wrote against the page in your browser returns nothing, because the content only exists after the JavaScript runs and the API calls behind it return.
Running headless Chrome yourself fixes the rendering and brings new problems: when is the page done? Waiting for the load event fires too early on apps that fetch data afterwards, a fixed sleep is either too short or wastes seconds on every request, and the browser fleet needs memory, updates and crash handling.
The API decides per page whether it needs a browser: prerender defaults to auto, and true forces rendering. Then waitForSelector holds extraction until the element you are about to read exists, so the rules run against the same DOM a visitor sees. The page preparation guide explains each wait option.
How to scrape a JavaScript-rendered page with a real browser
Force the browser, wait for the element, extract. Then tighten the wait so each request spends as little time as possible.
import createClient from 'microlink.io'
const microlink = createClient({
apiKey: process.env.MICROLINK_API_KEY
})
const { quotes } = await microlink.extract(
'https://quotes.toscrape.com/js/',
{
quotes: {
selectorAll: '.quote',
attr: {
text: { selector: '.text', attr: 'text' },
author: { selector: '.author', attr: 'text' }
}
}
},
{ prerender: true, waitForSelector: '.quote' }
)This demo page builds its quotes with JavaScript, so the plain HTML has none. With prerender and waitForSelector, quotes resolves to an array of { text, author } objects.
import createClient from 'microlink.io'
const microlink = createClient({
apiKey: process.env.MICROLINK_API_KEY
})
const { jobs } = await microlink.extract(
'https://app.example.com/jobs',
{ jobs: { selectorAll: '[data-testid=job-title]', attr: 'text' } },
{
prerender: true,
waitUntil: 'domcontentloaded',
waitForSelector: '[data-testid=job-title]'
}
)waitUntil domcontentloaded ends navigation as soon as the DOM is parsed, and the selector wait covers the data that loads afterwards. This is usually faster than waiting for the network to go idle.
import createClient from 'microlink.io'
const microlink = createClient({
apiKey: process.env.MICROLINK_API_KEY
})
const { props } = await microlink.extract(
'https://nextjs-app.example.com/product/42',
{
props: {
evaluate: "JSON.parse(document.getElementById('__NEXT_DATA__').textContent).props.pageProps",
type: 'object'
}
},
{ prerender: true }
)evaluate runs JavaScript in the page and returns the result. Many frameworks embed their initial data as JSON, which is often cleaner than the rendered markup.
- prerender auto decides per page. true forces a headless browser, false a plain HTTP fetch.
- waitForSelector Waits until an element matching the CSS selector appears. The most reliable wait.
- waitUntil Navigation event to wait for: auto, load, domcontentloaded, networkidle0 or networkidle2.
- waitForTimeout A fixed delay for pages with no stable selector. Capped by the request timeout.
- evaluate A rule that runs JavaScript in the page context instead of querying the DOM.
- javascript Set false to render without running scripts when the page does not need them.
The x-fetch-mode response header tells you which path ran: prerender when a browser rendered the page, fetch for a plain request. If fields are still null after the wait, the troubleshooting guide walks through wrong selectors, page variants and timeouts.
Why rendering on the API side beats running your own browser
The browser is the expensive part of scraping a React website. Moving it behind the request leaves you with the part that is specific to your job: the selectors.
Pages that only reveal content after a click or a scroll need one more step: scrape paginated lists and Load more buttons covers both.
The caching and performance guide lists the extraction speedups in order of impact, starting with meta: false.
When not to: if you only need the page title, description and image of an app, metadata from single-page apps returns them without writing rules. For the full text, use Markdown from JavaScript-rendered pages.
FAQ
How do I scrape a React website that returns empty HTML?
Why does my SPA scraping request return null fields?
Should I use waitForSelector or waitForTimeout on a JavaScript page?
How do I know whether the page was scraped with a browser?
Can I scrape the JavaScript state of a page instead of its DOM?
Solve the next problem with the same API
Any website to JSON
Pagination and Load more buttons
Puppeteer without hosting Chrome
Metadata from single-page apps
Markdown from JavaScript-rendered pages
Screenshots of JavaScript-rendered pages
Ready to scrape JavaScript apps?
A real browser, the right wait and your rules in one request. Start on the free tier and scrape your first single-page app today.