TL;DR
Quickest embed:<iframe> / <object> (zero JS, zero features).
Most popular OSS: react-pdf (PDF.js under the hood; easy).
Headless control: use pdfjs-dist directly for custom UI/virtualization.
Feature‑heavy: Apryse WebViewer, PSPDFKit, Foxit Web SDK.
Next.js: client‑only viewer, load the worker, enable byte‑range.
:::
Options at a glance
| Option | License | Strengths | Gaps / Caveats | Good for |
|---|---|---|---|---|
<iframe> / <object> / native browser viewer | Free | Fastest to integrate; no deps | Limited features; browser‑dependence; origin/CSP issues | Internal tools, quick previews |
| react-pdf (PDF.js) | MIT | Simple component API; text selection; basic forms | Bundle size; pagination perf for huge PDFs; SSR quirks | Product UIs with standard needs |
| pdfjs-dist (raw PDF.js) | Apache‑2.0 | Max control; tune rendering/virtualization | Build your own UI; more plumbing | Custom readers, infinite scroll |
| Apryse WebViewer (PDFTron) | Commercial | Rich annotations, forms, redaction, collaboration, Office files | Licensing cost; heavier bundle | Enterprise workflows |
| PSPDFKit for Web | Commercial | Polished UX; signing, stamping, collaboration | Licensing cost | Document‑heavy SaaS |
| Foxit Web SDK | Commercial | Strong enterprise features; performance | Licensing cost | Enterprise integrations |
| react-pdf-viewer / pdf-viewer-reactjs | OSS (varies) | Prebuilt toolbar/plugins, thumbnails | Maturity varies; plugin ecosystems differ | Faster OSS onboarding |
Libraries like pdf-lib or PDFKit focus on creation/manipulation, not end‑user viewing.
How PDF rendering works (why it matters)
- PDF.js (canvas/SVG): Most OSS viewers rely on Mozilla’s PDF.js to parse and render each page to
<canvas>(or sometimes SVG). Canvas is typically faster and more compatible on mobile; SVG may have better selectable text but can bloat the DOM. - Virtualization: Rendering only visible pages prevents memory blowups—vital for 500+ page docs on mobile.
- Workers: Offload parsing to a Web Worker (
pdf.worker.js). Without it, the main thread janks. - Range Requests: Enable HTTP byte‑range so users can open page 300 without downloading everything.
1) The simplest path: embed the browser’s viewer
X-Frame-Options or strict CSP.
Tip: For public files, link‑fallback to the native viewer when JS fails.
2) react-pdf (most popular OSS)
Install
- Componentized API for React.
- Built‑in text selection and (basic) form support.
- Good community docs.
- Disable SSR for the viewer component (Next.js).
- Large files: render current + nearby pages only to avoid memory spikes.
3) Headless: PDF.js (pdfjs-dist) with custom UI
Install
- Full control over layout, virtualization, page cache, search indexing, and UI.
- Tailor performance for massive docs.
- You build the toolbar, thumbnails, text selection, and accessibility affordances.
4) Full‑featured commercial SDKs
When you need annotations, comments, collaboration, e‑signing, redaction, measurement, forms, or Office file viewing, consider:- Apryse WebViewer (formerly PDFTron)
- PSPDFKit for Web
- Foxit Web SDK
Next.js / Vite integration notes
- Workers must be served correctly. If hosting yourself, copy
pdf.worker.min.jstopublic/and setworkerSrcaccordingly. - Range Requests: serve PDFs via a CDN/origin that supports
Accept-Ranges: bytes. - CORS: if loading cross‑origin, enable CORS and set
Content-Type: application/pdf. - Printing: PDF.js has its own print flow; test on Safari and iOS specifically.
Accessibility and i18n
- Provide keyboard navigation (page up/down, space, arrows).
- Ensure the text layer is present for selection and screen readers.
- Add aria labels for toolbar controls.
- Support RTL locales and mixed‑language content.
Performance checklist
- Use virtualized pages (render only what’s visible).
- Cache rendered canvases; reuse when zooming.
- Preload next/prev page on idle.
- Keep scale < 2.0 on mobile to avoid massive canvases.
- Debounce zoom/scroll events.
- Offload heavy work to Web Workers.
Security checklist
- Don’t execute PDF contents; treat links as untrusted.
- Sanitize
mailto:/ blockjavascript:links; open external links withrel="noopener noreferrer". - Respect
Content-Security-Policyand considerframe-ancestors/X-Frame-Optionswhen embedding.
Choosing: a decision tree
- Just preview a PDF? Try
<object>/<iframe>(fast). If you need consistent zoom/print, usereact-pdf. - Custom UI & performance tuning? Build with
pdfjs-distdirectly; add virtualization. - Annotations/forms/signing/redaction? Choose a commercial SDK (Apryse/PSPDFKit/Foxit).
- Massive files / mobile heavy? Headless PDF.js + virtualization + worker + CDN with byte‑range.
Gotchas & debugging
- Blank pages: worker not found → double‑check
workerSrcpath. - Slow first paint: no range requests → enable
Accept-Ranges, or load from same origin. - CORS errors: add
Access-Control-Allow-Originand correctContent-Type. - SSR crash: dynamic import with
ssr:falseand guard allwindow/documentaccess. - Huge memory: render 1–3 pages around viewport; recycle canvases.
Quick recommendations
- General product UI:
react-pdf+ light toolbar; add virtualization when PDFs exceed ~200 pages. - Heavily customized experience:
pdfjs-dist+ your own UI/virtualization. - Enterprise doc workflows: Apryse/PSPDFKit/Foxit—budget permitting.