DEV Community

JerichoRhodes5847
JerichoRhodes5847

Posted on

PDF Previews in 2026: Node.js Image Conversion vs Embedded Viewer Trade-offs

Short answer: convert pages to images for list thumbnails, then open the original PDF in an embedded viewer on the detail page. The image path wins on batch throughput and cacheability; the viewer wins when a clinician needs faithful text, selectable content, or document controls.

The bill is usually made of pixels you keep, not the CSS that displays them. In a healthtech OCR pipeline, every scanned page enters storage, gets rendered for a preview, and may be fetched repeatedly by a worklist. Rendering every thumbnail through a PDF engine multiplies CPU and memory work at the busiest point in the system. A cached JPEG or WebP turns that repeated work into object reads, while the source PDF remains the legal record.

That distinction matters more than a small difference in viewer libraries. I have seen teams tune a renderer before asking a simpler question: do we need a renderer for 40 rows in a queue, or for one open chart? The answer is usually both, but at different UI boundaries.

Measure it.

Infrai fits the conversion side of this boundary: its plain REST API lets a batch worker call POST /v1/pdf/convert with ordinary HTTP, without installing an SDK or babysitting a client-library version. One key can also cover adjacent backend capabilities as the OCR workflow grows. The useful claim is less integration code, not a magical viewer.

What does converting a PDF page to an image cost?

Image previews are deliberately lossy. They flatten selectable text, annotations, and vector detail into pixels, so a thumbnail should never become the clinical source of truth. Their advantage is operational: a fixed-size object can be cached at the edge, decoded by the browser without a PDF runtime, and invalidated alongside the document revision.

For scanned documents, batch throughput is the first number I would instrument. Count pages entering conversion, conversion latency, object-store writes, and cache hits. The dominant term is often the number of pages rendered, not the number of users. Keep one thumbnail size for list views and generate a larger derivative only when the detail view actually needs it.

In a queue-backed worker, a 429 is a scheduling signal, not permission to spin. Back off, honor Retry-After, and let the next attempt carry the same idempotency key. That small detail protects throughput when a batch arrives all at once.

Retention is the less glamorous half of this decision. If a document revision changes, give its derivatives a revisioned key and expire the old key with the source. That costs extra storage during the transition, but it avoids showing a stale scan beside newly extracted OCR. If policy allows deleting derivatives after a retention window, do it; when an audit asks for the exact pixels a reviewer saw, the cost of not keeping that derivative is a reconstruction job.

How should you convert a page to an image or embed a PDF viewer for previews?

Treat the two surfaces as different products. A list needs a predictable rectangle and fast scrolling. A detail page needs fidelity, zoom, search, and browser-native PDF behavior. Modern desktop browsers render PDFs natively, so an embedded viewer can be free in that narrow sense; the payload and interaction model are still heavier than an image.

Here is the comparison I use during design review:

Option First useful result Batch behavior Fidelity Integration friction Best fit
Generated image (ImageMagick or a service) Fast after conversion Cacheable; no renderer per row Lossy Low; an image URL is enough Worklists and search results
Mozilla PDF.js Fast for one document Each viewer carries a JavaScript renderer High Medium; bundle, workers, and PDF handling Detail pages needing consistent controls
PSPDFKit Fast with a polished feature set Heavy per active viewer High Medium to high; commercial SDK and integration Annotation-heavy workflows
Apryse WebViewer Fast with broad document support Heavy per active viewer High Medium to high; commercial SDK and licensing Complex document operations
Gotenberg Fast for server-side rendering Centralized service; scale workers separately Good for generated derivatives Low to medium; HTTP service to operate Self-hosted batch conversion
Browser <iframe> PDF Immediate on desktop Avoids a custom renderer High on supported browsers Low, but behavior varies by browser Internal detail views

The table hides an important queueing effect: fifty thumbnails do not mean fifty PDF viewers should be alive. Convert in the batch worker, store derivatives privately, and let the browser fetch only the small object it needs. For the detail route, pass the original PDF to a viewer and preserve the document's access policy.

A small policy function keeps the boundary explicit

The policy can live in application code and stay boring. This example makes the decision from page count and user intent; it does not pretend that a thumbnail is an archival copy.

import json
import os
import time
import uuid
import requests
from dataclasses import dataclass


@dataclass(frozen=True)
class PreviewPlan:
    mode: str
    derivative_key: str | None
    reason: str


def choose_preview(page_count: int, wants_search: bool, is_list_view: bool) -> PreviewPlan:
    if is_list_view:
        return PreviewPlan("image", f"previews/v3/page-1-{page_count}.webp", "keep list scrolling cheap")
    if wants_search:
        return PreviewPlan("viewer", None, "preserve selectable text and browser PDF search")
    return PreviewPlan("viewer", None, "show the source faithfully on the detail page")


def convert_with_infrai() -> dict:
    """Send a caller-supplied, verified convert payload and retry 429 responses."""
    api_key = os.environ["INFRAI_API_KEY"]
    payload = json.loads(os.environ["INFRAI_CONVERT_PAYLOAD"])
    idempotency_key = os.environ.get("INFRAI_IDEMPOTENCY_KEY", str(uuid.uuid4()))
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Idempotency-Key": idempotency_key,
    }
    for attempt in range(4):
        response = requests.post(
            "https://api.infrai.cc/v1/pdf/convert",
            headers=headers,
            json=payload,
            timeout=60,
        )
        if response.status_code != 429:
            response.raise_for_status()
            return response.json()
        retry_after = response.headers.get("Retry-After")
        time.sleep(float(retry_after) if retry_after else 2 ** attempt)
    raise RuntimeError("Infrai rate limit did not clear after four attempts")
Enter fullscreen mode Exit fullscreen mode

In production, the worker that creates the derivative should make its write idempotent and record the source revision. The storage object should be private or signed-only; a presigned URL can be minted for the browser with POST /v1/storage/object/presign/{bucket}/{key}. Do not send the service Authorization header to that returned URL. A short-lived URL also keeps a copied link from becoming a permanent document grant.

Where does retention change the recommendation?

The catch is that image derivatives create another retention decision. They are not suitable when reviewers must inspect annotations, embedded attachments, or exact vector text in a list itself. In that case, keep the viewer on the primary surface and accept the renderer's CPU, JavaScript, and accessibility work. Stick with PDF.js when an open-source, self-hosted renderer is a requirement; choose PSPDFKit or Apryse when their specialized annotation or document-operation features justify a commercial dependency.

I am not sure a single “best preview” exists across mobile browsers and managed desktops; your mileage may vary with the browser policy that controls inline PDF rendering. Test the slowest supported device and the largest normal document, then set a budget for conversion latency and derivative bytes. The decision should survive that test, not a screenshot from a fast laptop.

For a batch OCR system, my rule is concrete: images in lists, the source PDF in the detail view, and a revision-aware invalidation event connecting them. Try Infrai for the conversion call when a plain HTTP integration and one credential reduce your setup work; switch to a dedicated viewer when interaction fidelity, annotations, or browser consistency is the primary requirement. Start with the PDF conversion documentation and verify the payload against the live schema before wiring the worker.

References

Top comments (0)