Marketing

Choosing a Reader for Each Page Type: HTML, PDF and JS-Rendered

Antonio Evans9 min read

A simple three-column decision table with rows for static HTML, PDF, and JS-rendered pages.
Key points

Key points

  1. 1Start every URL with a plain HTTP fetch and only pay for a browser when that fetch returns empty or script-only text.
  2. 2Route files that end in .pdf, or that arrive as application/pdf, straight to a PDF text parser before anything touches a browser.
  3. 3Score every extraction on length, unique-word ratio, boilerplate share and presence of required fields, then escalate to the next reader when the score fails.
  4. 4Log reader, latency, bytes and quality score per page type so the routing table is driven by your own measurements, not by guesses.

Start with the cheapest reader that can work

Every page you fetch has a cost, and the cost gap between readers is large. A plain HTTP client such as curl, requests or fetch downloads bytes and hands you the response body. A headless browser launches a real engine, parses HTML, executes scripts, resolves styles and often loads dozens of subresources before the DOM settles. A PDF parser reads a byte stream and reconstructs characters and lines from drawing instructions.

That ordering is also the order of reliability. The cheapest reader that returns clean text is the one you should try first, and the browser is a fallback rather than a default. The reason is simple: server-side rendering sends the content in the first response, so a single request is enough to read it, while client-side rendering builds the page after the response arrives, and an HTTP client alone will not see data that appears only after scripts run (Browserless).

A useful mental model separates three jobs that people often blur together. Crawling decides which pages to visit, scraping collects content from a page, and extraction maps values into fields. Each has its own failure mode, and a pipeline can reach every URL while still extracting the wrong value, or extract perfect records from only part of the intended set (webscraper.io). Reader choice is a scraping decision, quality scoring is an extraction decision, and you should measure them separately.

Keep the default policy blunt:

  1. If the response body is a PDF, send it to the PDF parser.
  2. If the body is HTML with the target text in the initial response, keep the HTTP result.
  3. If the body is a thin shell, an empty app root, or a script-only stub, escalate to a headless render.
  4. If any reader produces text that fails the quality gate, escalate one level and record the failure.

The rest of this article fills in how to classify, how to score, and what the cost looks like with numbers.

A decision table that maps page type to reader

The table below is the core of the article. It assumes the URL is known and the goal is clean text, not screenshots or interaction.

Page typeSignals in the first responseStart withEscalate toWhy
Static HTML article, docs page, blog postTarget text and links present in body, meaningful heading tags, low script-to-text ratioPlain HTTP fetchHeadless render only if quality gate failsContent arrives in the first response, so a browser adds cost without adding text (Trendyol Tech)
Server-rendered app page (SSR)Full markup in response, hydration scripts present but content already in DOMPlain HTTP fetchHeadless render if a field is missingSSR sends all content from the server on the initial response, no JavaScript execution needed (Trendyol Tech)
Client-rendered app (SPA, infinite scroll, click-to-load)Empty root element, no target text, bundle scripts, JSON bootstrappingHeadless renderPlain HTTP fetch against an underlying JSON endpoint if you can find oneContent is built after the initial response, so a plain client sees nothing (Browserless)
PDF document served as a fileContent-Type application/pdf, or .pdf path, or %PDF headerPDF text parserLearning-based parser or OCR for scanned pagesPDFs store placement instructions rather than tagged structure, so text extraction is a reconstruction problem (Adhikari and Agarwal)
PDF served inside a viewer pageHTML shell with an embedded viewer, file behind a separate requestFetch the underlying PDF, then parseHeadless render only to discover the file URLThe viewer is a script; the text lives in the file
Table-heavy report or financial filingMany numeric cells, multi-column layoutPDF parser with table support, or learning-based toolHeadless render candidate only if the same data exists as HTMLParsers differ sharply by document category, and table detection varies by tool (Adhikari and Agarwal)
Login-gated or interaction-gated pageRedirect to sign-in, consent wall, or content behind a clickHeadless renderEscalate to a managed browser service or stopSession handling and clicks require a real engine, not just a fetch (Browserless)

Two rows deserve emphasis. The SPA row is where teams waste the most money, because a browser is the only reader that works but a JSON endpoint is often cheaper still. The PDF-in-a-viewer row is where teams waste the most time, because the URL they logged is not the URL that contains the text.

Worked example: cost per 10,000 pages by reader

Numbers make the choice concrete. The figures below are illustrative unit costs you should replace with your own measured latency and your provider's billing, but the arithmetic is the point. Suppose a plain fetch takes about 0.2 seconds of wall clock and a few hundred kilobytes, a headless render takes several seconds and loads many subresources, and a PDF parse takes well under a second per page for born-digital files.

ReaderPagesSeconds per page (assumed)Total wall clockBrowser fleet neededTypical failure mode
Plain HTTP fetch10,0000.2~33 minutesnoneEmpty shell, bot block
Headless render10,0004.0~11 hoursmultiple concurrent instancesTimeouts, detection, memory
PDF text parser10,0000.3~50 minutesnoneScanned pages, broken columns

If 70 percent of a 10,000 page batch is static HTML, 20 percent is PDF and 10 percent truly needs a browser, routing by page type keeps the browser fleet at roughly 1,000 pages instead of 10,000. That is a tenfold cut in the most expensive resource. A managed crawl service prices the same distinction explicitly: each crawled page consumes one credit, with PDF parsing billed per PDF page and structured JSON extraction billed as additional credits on top (Firecrawl). The lesson generalizes whether you self-host or buy: the render is the line item, so avoid it where a cheaper reader wins.

There is a second cost that rarely appears in budgets: blocked requests. Sites watch for datacenter IP ranges, missing browser headers and inhuman request rates, and a naive loop around a bare HTTP client gets rate-limited, IP-blocked or served a CAPTCHA before reaching the content (Crawlbase). A browser does not fix blocking by itself. Stealth patches, proxy rotation and managed fingerprint handling each close part of the detection gap and none closes all of it (Browserless). Budget for proxies separately from readers.

Quality gates: detect garbage before it reaches your pipeline

Cheap readers sometimes return text that is technically non-empty and completely useless: cookie banners, navigation, a "enable JavaScript" notice, or a garbled column dump. The fix is a deterministic gate that runs on every extraction before it is stored, and it should be code, not a model. Deterministic checks are the right tool when a rule can express the expectation, and models are for judging meaning (Arize).

A practical gate uses four checks and a points system:

  • Length: body text characters after boilerplate removal. A product page under roughly 200 characters is suspicious.
  • Unique-word ratio: unique words divided by total words. Very low ratios indicate repeated menus or template text.
  • Boilerplate share: characters inside nav, header, footer and aside elements divided by total characters. High share means the main content did not load.
  • Required fields: for structured jobs, the presence of the fields your schema demands, such as title and date.

Here is a compact implementation of that gate in Python, scoring each extraction from 0 to 4 and routing accordingly:

import re
from urllib.parse import urlparse

BOILERPLATE = re.compile(r"<(nav|header|footer|aside)[^>]*>.*?</\1>", re.S | re.I)
TAGS = re.compile(r"<[^>]+>")

def text_of(html: str) -> str:
    return re.sub(r"\s+", " ", TAGS.sub(" ", html)).strip()

def quality(html: str, required=("title",)) -> tuple[int, dict]:
    stripped = BOILERPLATE.sub(" ", html)
    body = text_of(stripped)
    full = text_of(html)
    words = body.split()
    unique_ratio = len(set(w.lower() for w in words)) / max(len(words), 1)
    boiler_share = 1 - (len(body) / max(len(full), 1))
    score, detail = 0, {
        "chars": len(body),
        "unique_ratio": round(unique_ratio, 3),
        "boilerplate_share": round(boiler_share, 3),
    }
    if len(body) >= 200:
        score += 1
    if unique_ratio >= 0.30:
        score += 1
    if boiler_share <= 0.50:
        score += 1
    lower = body.lower()
    if all(tag in lower for tag in required):
        score += 1
    return score, detail

def route(url: str, fetch, render, parse_pdf):
    path = urlparse(url).path.lower()
    if path.endswith(".pdf"):
        result = parse_pdf(url)
        if quality(result["html"])[0] >= 3:
            return "pdf", result
    fetched = fetch(url)
    score, detail = quality(fetched["html"])
    if score >= 3:
        return "http", fetched
    rendered = render(url)
    if quality(rendered["html"])[0] > score:
        return "render", rendered
    return "quarantine", fetched

Two design notes. First, the thresholds are starting points, not laws: calibrate them on a labeled sample of a few hundred pages and watch the false escalation rate. Second, the gate belongs in the extraction layer, not the retrieval layer, because extraction accuracy is measured at the field level, against a verified ground truth, and is task specific, so an invoice benchmark does not transfer unchanged to contract clauses (LlamaIndex).

The parsing layer deserves its own warning. Optical character recognition converts visual content to machine-readable text imperfectly, and character errors, layout misinterpretation and inconsistent table handling propagate straight into extraction, corrupting data before your field logic ever runs (LlamaIndex). A scanned PDF that produces 400 characters of gibberish should fail the gate and escalate, not enter the index.

What the PDF parser can and cannot do

PDF is not a web page. It stores instructions for character and line placement rather than tagged structure, which is why parsing is a reconstruction problem: word identification, word order, paragraph integrity and tables all break in predictable ways (Adhikari and Agarwal). That property drives reader choice inside the PDF family.

The largest comparative study of the question evaluated ten popular tools across six document categories using the DocLayNet dataset. PyMuPDF and pypdfium generally outperformed other text extractors, all parsers struggled with scientific and patent documents, learning-based tools such as Nougat did better on those hard categories, and table detection quality varied by document type, with Table Transformer strong on financial, patent, law and scientific categories and Camelot best on government tenders (Adhikari and Agarwal). A separate hands-on test of seven Python extractors on a one-page business document with a six-column table found pypdfium2 returned clean text in about 0.004 seconds, fast and readable but without table structure (Aman Kumar).

The decision rule that follows is simple. For born-digital PDFs where you need prose, start with a fast rule-based parser. When the gate reports low length, broken word order or missing tables on scientific, patent or financial files, escalate within the PDF family to a layout-aware or learning-based tool before you consider a browser. A browser should almost never be your PDF reader, because rendering a PDF in a browser gets you pixels or a viewer, not structured text.

Logging cost and quality per page type

You cannot tune a routing table you do not measure. Emit one log record per URL with enough fields to recompute the decision later. A minimal schema looks like this:

{
  "url": "https://example.com/report",
  "page_type": "pdf",
  "reader": "pdf_parser",
  "escalated_from": "http",
  "latency_ms": 310,
  "bytes_in": 812344,
  "chars_out": 18422,
  "quality_score": 4,
  "required_fields_found": 2,
  "required_fields_total": 2,
  "http_status": 200,
  "outcome": "kept"
}

Then review three aggregates weekly: escalation rate by page type, quality score distribution by reader, and cost per thousand clean pages. If the escalation rate for a site climbs, the site changed its rendering and your page-type classifier is stale. If quality scores for the HTTP reader drop while escalations stay flat, your boilerplate selectors broke. Keep function and evaluator versions next to results when comparing runs, so a change in scoring is not mistaken for a change in extraction quality (Arize).

For open-ended downstream tasks, such as summarizing a page or answering questions over it, deterministic gates are not enough. There, an LLM judge with a rubric can score relevance or groundedness against the task input and any reference material, returning labels and scores you can aggregate (Arize). Judges are a flexible, fast, cost-effective approximation of human judgment, and their usefulness depends on the prompt, the model and the task complexity (Evidently AI). Validate the judge on reviewed examples before you trust its scores, and keep it out of the hot path where a regex would do.

Finally, structure your outputs so a bad extraction cannot silently corrupt a database. When a page yields text, hand it to a model with a JSON schema and require the response to conform, so required keys are present and enum values are valid rather than hoped for (OpenAI). Where a provider lacks native schema enforcement, schema validation plus a recovery pass still catches most failures, because the majority of extraction failures are systematic and correctable formatting problems rather than semantic misunderstanding (MDPI Algorithms). That is the same philosophy as the reader gate: fail loudly, escalate once, and log why.

If you are running this at scale across mixed sources, the natural next step is to keep the reader logic and push the model calls through one endpoint. Quanta Inference (https://quantaintelligence.ai/inference) serves text, image, video, audio and embedding models through one OpenAI-compatible API key with prepaid US dollar pricing, so the judge and the schema-bound extractor do not each need their own integration.

FAQ

FAQ

Why not always use a headless browser?

Because it is the most expensive reader and the slowest, and most pages do not need it. Server-rendered HTML arrives complete in the first response, so a plain fetch reads it without executing scripts (Trendyol Tech). Reserve the browser for pages whose content appears only after scripts run.

How do I tell a JavaScript-rendered page from a static one cheaply?

Fetch it once and inspect the body: if the target text and links are absent while script bundles and an empty app root dominate, treat it as client-rendered. A browser exists precisely because an HTTP client cannot see data that renders after the initial response (Browserless). Log that signal so the classifier improves over time.

What counts as garbage output?

Text that is non-empty but lacks the content you came for: short length, very low unique-word ratio, high boilerplate share or missing required fields. Score it deterministically and escalate one reader level when the score fails, because code-based checks are the right tool when a rule can express the expectation (Arize).

Which PDF parser should I start with?

A fast rule-based extractor such as PyMuPDF or pypdfium2 for born-digital prose, since both generally outperformed other text extractors in a ten-tool comparison (Adhikari and Agarwal). Escalate to a layout-aware or learning-based tool when the document category is scientific, patent or table-heavy, where simple parsers struggle.

Can a fallback chain ever cost more than using the browser first?

Yes, when most pages in a batch are client-rendered, because you pay for a wasted fetch on every URL before escalating. Measure the escalation rate per site and flip the default for sites above roughly half, keeping the fetch as a cheap probe rather than a first attempt (webscraper.io).

Sources

Sources

  1. Best Headless Browser for Scraping: 8 Tools Compared
  2. Web crawling vs web scraping vs data extraction
  3. Rendering 2 billion pages with a headless browser for ...
  4. A Comparative Study of PDF Parsing Tools Across Diverse ...
  5. Crawl
  6. How to Crawl Data from a Website: a practical Python ...
  7. LLM-as-a-Judge: Guide & Pre-built Evaluators
  8. What is Extraction Accuracy Benchmarking?
  9. I Tested 7 Python PDF Extractors So You Don't Have To (2025 ...
  10. LLM-as-a-judge: a complete guide to using LLMs for ...
  11. Structured model outputs | OpenAI API
  12. Efficient and Verified Research Data Extraction with LLM
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles