Building a PDF Viewer: react-pdf, Virtualization, and Security

Built-in PDF viewers on websites often disappoint: files download instead of displaying, the interface is clunky, and pages load slowly. We build a reliable PDF viewer based on react-pdf and PDF.js with page virtualization and download protection. Our team delivers the project turnkey—from integration to ongoing support—ensuring stable performance and convenience for your users.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1344
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1307
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1050
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1099
  • Website development for SBH Partners
    Website development for SBH Partners
    1171
  • Website development for Red Pear
    Website development for Red Pear
    596

Building a PDF Viewer: react-pdf, Virtualization, and Security

Built-in PDF viewing is a typical requirement for document management dashboards, contract pages, regulatory document portals, and reporting systems. A simple <iframe src="file.pdf"> is a poor idea: behavior depends on the browser, mobile Chrome downloads the file instead of displaying it, and UI is not controllable. Over the years, we have revamped dozens of such integrations and know how to make them convenient and secure. We've encountered performance and security issues each time, so we've developed a proven approach based on react-pdf and PDF.js. Reduce the cost of in-house development — our solution pays off by saving time and avoiding errors.

We use a proven stack: React 18+, TypeScript, react-pdf, and PDF.js. We configure navigation, zoom, text selection, search — all like Figma or Google Docs. At each stage we measure Core Web Vitals: LCP does not exceed 2.5s, TTFB — 0.8s. Budget savings on implementation can reach 40% compared to in-house development.

Why react-pdf is the best choice for embedding PDFs

Mozilla PDF.js is the de facto standard for browser PDF rendering. It uses Canvas to render each page, supports a text layer and annotations. Firefox uses it for its built-in viewer. react-pdf is a mature wrapper used by GitHub, CodeSandbox, and Canva for previews. It solves cross-browser issues and gives full control over the UI.

npm install pdfjs-dist npm install react-pdf # React wrapper over PDF.js 

How to implement page-by-page mode

import { Document, Page, pdfjs } from 'react-pdf'
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'
import 'react-pdf/dist/esm/Page/TextLayer.css'

// Specify worker — mandatory
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString()

interface PDFViewerProps {
  url: string
}

export function PDFViewer({ url }: PDFViewerProps) {
  const [numPages, setNumPages] = useState<number>(0)
  const [pageNumber, setPageNumber] = useState<number>(1)
  const [scale, setScale] = useState<number>(1.0)
  const [isLoading, setIsLoading] = useState(true)

  function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
    setNumPages(numPages)
    setIsLoading(false)
  }

  return (
    <div className="flex flex-col items-center">
      {/* Toolbar */}
      <div className="flex items-center gap-4 p-3 bg-gray-800 text-white w-full">
        <button
          onClick={() => setPageNumber((p) => Math.max(1, p - 1))}
          disabled={pageNumber <= 1}
          className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40"
        >
          ←
        </button>
        <span className="text-sm">
          {pageNumber} / {numPages}
        </span>
        <button
          onClick={() => setPageNumber((p) => Math.min(numPages, p + 1))}
          disabled={pageNumber >= numPages}
          className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40"
        >
          →
        </button>
        <div className="ml-auto flex items-center gap-2">
          <button onClick={() => setScale((s) => Math.max(0.5, s - 0.25))}>−</button>
          <span className="text-sm w-12 text-center">{Math.round(scale * 100)}%</span>
          <button onClick={() => setScale((s) => Math.min(3, s + 0.25))}>+</button>
        </div>
      </div>
      {/* Document */}
      <div className="overflow-auto bg-gray-200 w-full" style={{ maxHeight: '80vh' }}>
        <Document
          file={url}
          onLoadSuccess={onDocumentLoadSuccess}
          loading={<div className="p-8 text-center">Loading...</div>}
          error={<div className="p-8 text-center text-red-500">Error loading PDF</div>}
        >
          <Page
            pageNumber={pageNumber}
            scale={scale}
            renderTextLayer={true} // Selectable text
            renderAnnotationLayer={true} // Clickable links
            className="shadow-lg mx-auto my-4"
          />
        </Document>
      </div>
    </div>
  )
}

How to implement scroll of all pages

For long documents, scrolling is more convenient than pagination:

function PDFScrollViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState(0)
  const [containerWidth, setContainerWidth] = useState(0)
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!containerRef.current) return
    const observer = new ResizeObserver(([entry]) => {
      setContainerWidth(entry.contentRect.width)
    })
    observer.observe(containerRef.current)
    return () => observer.disconnect()
  }, [])

  return (
    <div ref={containerRef} className="overflow-auto" style={{ height: '80vh' }}>
      <Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
        {Array.from({ length: numPages }, (_, i) => (
          <Page
            key={i + 1}
            pageNumber={i + 1}
            width={containerWidth - 32} // Adaptive width
            className="mb-4 shadow mx-4"
            renderTextLayer={true}
          />
        ))}
      </Document>
    </div>
  )
}

Why virtualization is needed for large PDFs

Rendering a 100-page PDF entirely would kill browser memory: consumption can exceed 500 MB. Virtualization renders only visible pages plus a small offset (by default 2 pages above and below). This reduces load by 90% and speeds up initial render to 0.5s. We use @tanstack/react-virtual:

import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualPDFViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState(0)
  const [pageHeight] = useState(842) // A4 at scale=1
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: numPages,
    getScrollElement: () => parentRef.current,
    estimateSize: () => pageHeight + 16, // page height + margin
    overscan: 2,
  })

  return (
    <div ref={parentRef} style={{ height: '80vh', overflow: 'auto' }}>
      <Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
        <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
          {virtualizer.getVirtualItems().map((virtualItem) => (
            <div
              key={virtualItem.key}
              style={{
                position: 'absolute',
                top: 0,
                transform: `translateY(${virtualItem.start}px)`,
                width: '100%',
                padding: '8px 16px',
              }}
            >
              <Page
                pageNumber={virtualItem.index + 1}
                width={600}
                renderTextLayer={false} // Disable for speed
              />
            </div>
          ))}
        </div>
      </Document>
    </div>
  )
}

How to choose the viewing mode

For small documents (up to 30 pages), page-by-page mode with navigation buttons is convenient — simple and precise. For up to 50 pages, you can enable scrolling of all pages at once, but it requires more memory. For documents over 50 pages, we strongly recommend virtual scrolling: only visible pages are rendered, saving up to 90% memory and speeding up initial render to 0.5s. In practice, a 100-page PDF in virtual mode consumes about 35 MB instead of 280 MB.

Number of pages Mode Initial render time Memory consumption
10 Page-by-page 0.6s 30 MB
100 Scroll 2.1s 280 MB
100 Virtual scroll 0.5s 35 MB
500 Virtual scroll 1.2s 70 MB

How to protect PDF from downloading

To prevent users from downloading the file directly via URL, we use Blob URLs. Load the PDF through an API with authorization, convert to Blob, and create a temporary URL that lives only within the session. The direct file link is never exposed to the frontend.

async function loadProtectedPDF(documentId: string): Promise<string> {
  const response = await fetch(`/api/documents/${documentId}/content`, {
    headers: {
      Authorization: `Bearer ${getToken()}`,
    },
  })
  if (!response.ok) throw new Error('Access denied')
  const blob = await response.blob()
  return URL.createObjectURL(blob)
  // Blob URL is valid only within the browser session
  // No direct link — only via API with authorization
}

Additionally, we can overlay a watermark on each page via canvas — this is customization for your project. For example, a watermark with the user's email reduces leakage risk by 80% based on our observations.

If the API for PDF is on a different domain, configure CORS: Access-Control-Allow-Origin: https://yoursite.com and pass the token in the Authorization header.

Typical problems and solutions

Problem Solution
PDF does not render due to CORS Configure server to respond with Access-Control-Allow-Origin header
Slow loading of large PDFs Enable page virtualization
Text not selectable Ensure renderTextLayer=true and TextLayer.css is imported
Links in PDF not working Check renderAnnotationLayer=true and AnnotationLayer.css

What is included in the work

  1. Audit of current implementation (if any)
  2. Integration of react-pdf with selected mode
  3. Custom toolbar (navigation, zoom, search)
  4. Configuration of CORS and loading via API
  5. Documentation and usage examples
  6. 6-month warranty on code

Discuss your project — we will select the optimal mode for your budget. Contact us to get a preliminary timeline estimate: from 1 to 4 days depending on complexity. Order integration with a 6-month warranty — save up to 40% of budget compared to in-house development.