← All posts

Scrape a Website With JavaScript: Why It Fails, How to Fix

September 23, 2026

Trying to scrape a website with JavaScript often starts with a simple fetch or axios call — and a confusing result: the response looks nothing like what your browser shows. The

you get back isn't broken; it's the whole page, before JavaScript ever runs. Understanding that gap is the first step to extracting data from dynamic sites, and it's where most tutorials skip too fast to Puppeteer-vs-Playwright debates without explaining the "why."

Why fetch() or axios Can't Scrape a JavaScript Website

Every HTTP client — fetch, axios, curl, Python's requests — does one thing: it asks a server for a document and prints whatever comes back. For a traditional server-rendered page, that document already contains your content. For a modern Single Page Application built with React, Vue, or Angular, the server sends a minimal HTML shell plus a bundle of JavaScript. The actual content — product listings, comments, prices — gets built afterward, inside a real browser, by executing that JavaScript against the DOM.

This is the core distinction between the HTML you fetch and the DOM you see. A browser downloads the HTML, then runs the scripts, then paints a fully assembled document tree. An HTTP client stops at step one. So when your scraper reports back an almost-empty page, it isn't a bug in your code — it's client-side rendering doing exactly what it's designed to do, just without a JavaScript engine present to trigger it. No amount of retrying the request, changing headers, or tweaking timeouts will fix this, because the missing piece isn't network-related. You need something that can actually execute JavaScript the way a browser does.

Two Ways to Actually Render JavaScript Before Scraping

Once you accept that plain requests won't cut it, there are two practical paths to rendering JavaScript for scraping.

Self-hosted headless browser. Tools like Puppeteer and Playwright drive a real instance of Chromium (or Firefox/WebKit, for Playwright) over the Chrome DevTools Protocol (CDP). They load the page like a visitor would, execute all scripts, and give you a completed DOM to query — often with Cheerio-style selectors or document.querySelector. This is genuine headless browser scraping: full fidelity, but you own the Chromium process, its memory footprint, and its crashes.

Managed rendering API. Instead of running Chromium yourself, you send a URL to a service that renders it server-side and hands back finished HTML, a screenshot, a PDF, or structured JSON. Browsevra's render API works this way — the JavaScript execution, waiting logic, and browser lifecycle happen on managed infrastructure, and your code just consumes the result. Same rendering problem, minus the operational overhead.

Neither approach is "cheating" — they solve the same core requirement (execute JS, then extract) at different points in your stack.

Tutorial: Scraping a JavaScript-Rendered Page

Here's the same task done both ways: load a page, wait for its dynamic content, and pull out text.

Self-hosted with Puppeteer (Node.js):

import puppeteer from "puppeteer";

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://example.com/products", { waitUntil: "networkidle0" });
await page.waitForSelector(".product-card");

const titles = await page.$$eval(".product-card h2", nodes =>
  nodes.map(n => n.textContent.trim())
);

console.log(titles);
await browser.close();

The key line is waitForSelector — it pauses execution until the dynamic content actually exists in the DOM, rather than assuming it loaded instantly. This is the single most important habit in any scrape-javascript-rendered-website tutorial: never extract before the render has settled.

Same task via the Browsevra render API:

curl -X POST https://api.browsevra.com/render \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/products",
    "waitForSelector": ".product-card",
    "format": "html"
  }'

One request, one response containing fully rendered HTML — ready for Cheerio parsing or direct JSON extraction if you request structured output. There's no browser to launch, no Chromium binary to keep updated, and no page.close() cleanup to remember. It's the same waitForSelector logic, expressed as an API parameter instead of a method call. Full request options are in the docs.

Common Mistakes That Break JavaScript Scrapers

A scraper that works on your laptop and fails in production almost always fails for one of these reasons:

  • Wrong wait condition. Waiting a fixed setTimeout instead of a selector or network-idle state means your scraper is racing the page, not reading it. Slow connections or A/B-tested layouts will break a hardcoded delay.
  • Lazy-loaded and infinite-scroll content. Many sites only render items as they enter the viewport. If your scraper never scrolls, it never triggers those loads — you'll extract the first ten rows of a hundred-row table and think the job is done.
  • Unblocked heavy resources. Letting every image, font, and analytics script load on each visit multiplies scrape time for no benefit. Blocking non-essential resource types at the browser level speeds up runs considerably.
  • Bot detection. Headless browsers have fingerprints — missing plugins, inconsistent headers, unusual navigator properties — that anti-bot systems flag. A scraper that works for ten requests can start returning CAPTCHAs or 403s at scale, especially without rotating infrastructure or handling detection gracefully.

Each of these is really an infrastructure and configuration problem, not a code logic one — which is why they tend to resurface once a script leaves your local machine.

Self-Hosted Browser or Managed API: How to Decide

If you're scraping a handful of pages occasionally — a personal project, a one-off data pull, a low-frequency monitor — a self-hosted Puppeteer or Playwright script is fine, and free open-source tooling can take you a long way (our decision guide on free scraper tools walks through those options).

The calculus changes at production scale. Once you're running concurrent scrapes across many domains, you're no longer just writing scraping logic — you're running a small browser farm: managing a pool of Chromium instances, rotating proxies, solving CAPTCHAs, watching memory usage, and scaling horizontally when traffic spikes. That's real infrastructure, and it competes for engineering time with the actual data problem you set out to solve.

This is the point where a managed rendering API earns its cost. You skip the Chromium fleet entirely and call an endpoint instead. If you want the full cost breakdown between self-hosting and paying for rendering as a service, see our detailed comparison — but if you're ready to try it now, the Browsevra render endpoint handles waiting, rendering, and extraction in one call, and pricing is transparent for testing against your own workload. No need to run and babysit your own Chromium fleet — browsevra does the rendering, you keep the data logic.

Frequently Asked Questions

Why does my scraper get empty HTML from a JavaScript website?

Because HTTP clients like fetch or axios only retrieve the server's initial response, which for JavaScript-heavy sites is a mostly empty HTML shell. The actual content is built afterward by client-side JavaScript running in a browser, so a request without a JS engine never sees it.

Can I scrape a JavaScript site without Puppeteer or Playwright?

Yes, by using a managed rendering API that executes the JavaScript server-side and returns finished HTML or structured data over a simple HTTP call — javascript scraping without puppeteer running locally, since browser execution happens on someone else's infrastructure.

Do I need a headless browser for every website?

No. If the data you need is present in the initial HTML response — check by viewing page source or disabling JavaScript in dev tools — a plain HTTP request and an HTML parser like Cheerio is faster and simpler. Reach for JS rendering only when content is missing from that raw response.

How do I know if a page needs JavaScript rendering before I scrape it?

Compare the raw HTML from a plain fetch or curl request against what you see in the browser's Elements panel. If data you need is absent from the raw response but visible in the rendered DOM, the site builds it client-side and requires a headless browser or rendering API.

Is scraping a JavaScript-rendered website legal?

It depends on jurisdiction, the site's terms of service, and what data you collect — publicly accessible, non-personal data is generally lower-risk than gated or personal information. Always review the target site's terms and applicable law, since rendering the page doesn't change the legal analysis of scraping it.

What's the fastest way to scrape JavaScript sites at scale without managing browser infrastructure?

A managed rendering API is the fastest path, since it removes the need to run, patch, and scale your own Chromium instances. Services like Browsevra handle the browser pool, waiting logic, and rendering server-side, so your code just requests a URL and receives finished HTML or data.