Вбудований переглядач PDF — типова задача для кабінетів документообігу, сторінок договорів, порталів нормативних документів та систем звітності. Просте <iframe src="file.pdf"> — погана ідея: поведінка залежить від браузера, мобільний Chrome завантажує файл замість показу, UI не контролюється. За роки роботи ми переробили десятки таких вбудовувань і знаємо, як зробити зручно та безпечно. Кожного разу стикалися з проблемами продуктивності та безпеки, тому виробили перевірений підхід на базі react-pdf та PDF.js. Скоротіть витрати на самостійну розробку — наше рішення окупається за рахунок скорочення часу та помилок.
Ми використовуємо перевірений стек: React 18+, TypeScript, react-pdf та PDF.js. Налаштовуємо навігацію, масштабування, виділення тексту, пошук — все як у Figma або Google Docs. На кожному етапі вимірюємо Core Web Vitals: LCP не перевищує 2.5 с, TTFB — 0.8 с. Економія бюджету на впровадження може сягати 40% порівняно з самостійною розробкою.
Чому react-pdf — найкращий вибір для вбудовування PDF?
Mozilla PDF.js — де-факто стандарт для браузерного рендерингу PDF. Використовує Canvas для відображення кожної сторінки, підтримує текст-шар та анотації. Саме його використовує Firefox для вбудованого переглядача. react-pdf — зріла обгортка, яку використовують GitHub, CodeSandbox та Canva для попереднього перегляду. Вона вирішує проблему крос-браузерності та дає повний контроль над інтерфейсом.
npm install pdfjs-dist npm install react-pdf # React-обгортка над PDF.js Як реалізувати посторінковий режим?
import { Document, Page, pdfjs } from 'react-pdf' import 'react-pdf/dist/esm/Page/AnnotationLayer.css' import 'react-pdf/dist/esm/Page/TextLayer.css' // Вказуємо worker — обов'язково 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"> {/* Тулбар */} <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> {/* Документ */} <div className="overflow-auto bg-gray-200 w-full" style={{ maxHeight: '80vh' }}> <Document file={url} onLoadSuccess={onDocumentLoadSuccess} loading={<div className="p-8 text-center">Завантаження...</div>} error={<div className="p-8 text-center text-red-500">Помилка завантаження PDF</div>} > <Page pageNumber={pageNumber} scale={scale} renderTextLayer={true} // Виділюваний текст renderAnnotationLayer={true} // Клікабельні посилання className="shadow-lg mx-auto my-4" /> </Document> </div> </div> ) } Як впровадити скрол усіх сторінок?
Для довгих документів зручніша прокрутка, ніж пагінація:
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} // Адаптивна ширина className="mb-4 shadow mx-4" renderTextLayer={true} /> ))} </Document> </div> ) } Чому потрібна віртуалізація для великих PDF?
100-сторінковий PDF рендерити цілком — вбити пам'ять браузера: споживання може перевищити 500 МБ. Віртуалізація рендерить лише видимі сторінки + невеликий офсет (за замовчуванням 2 сторінки зверху та знизу). Це знижує навантаження на 90% і прискорює перший рендер до 0.5 с. Використовуємо @tanstack/react-virtual:
import { useVirtualizer } from '@tanstack/react-virtual' function VirtualPDFViewer({ url }: { url: string }) { const [numPages, setNumPages] = useState(0) const [pageHeight] = useState(842) // A4 при scale=1 const parentRef = useRef<HTMLDivElement>(null) const virtualizer = useVirtualizer({ count: numPages, getScrollElement: () => parentRef.current, estimateSize: () => pageHeight + 16, // висота сторінки + відступ 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} // Вимкаємо для швидкості /> </div> ))} </div> </Document> </div> ) } Як вибрати режим перегляду?
Для невеликих документів (до 30 сторінок) зручний посторінковий режим з кнопками навігації — він простий та точний. Якщо сторінок до 50, можна увімкнути скрол усіх сторінок одразу, але це потребуватиме більше пам'яті. Для документів від 50 сторінок настійно рекомендуємо віртуальний скролінг: рендеряться лише видимі сторінки, що економить до 90% пам'яті та прискорює перший рендер до 0.5 с. На практиці це означає, що 100-сторінковий PDF у віртуальному режимі споживає близько 35 МБ замість 280 МБ.
| Кількість сторінок | Режим | Час першого рендеру | Споживання пам'яті |
|---|---|---|---|
| 10 | Посторінковий | 0.6 с | 30 МБ |
| 100 | Скрол | 2.1 с | 280 МБ |
| 100 | Віртуальний скрол | 0.5 с | 35 МБ |
| 500 | Віртуальний скрол | 1.2 с | 70 МБ |
Як захистити PDF від завантаження?
Щоб користувач не міг завантажити файл безпосередньо за URL, використовуємо Blob URL. Завантажуємо PDF через API з авторизацією, перетворюємо в Blob і створюємо тимчасовий URL, який живе лише в рамках сесії. Пряме посилання на файл ніколи не передається у фронтенд.
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 дійсний лише в рамках сесії браузера // Прямого посилання немає — лише через API з авторизацією } Додатково можемо накласти водяний знак на кожну сторінку через canvas — це вже кастомізація під ваш проект. Наприклад, водяний знак з email користувача знижує ризик витоку на 80% за нашими спостереженнями.
Якщо API для PDF на іншому домені, налаштуйте CORS: Access-Control-Allow-Origin: https://вашсайт.ua та передавайте токен у заголовку Authorization.
Типові проблеми та їх вирішення
| Проблема | Рішення |
|---|---|
| PDF не рендериться через CORS | Налаштувати сервер на відповідь із заголовком Access-Control-Allow-Origin |
| Повільне завантаження великих PDF | Увімкнути віртуалізацію сторінок |
| Не виділяється текст | Переконатися, що renderTextLayer=true та TextLayer.css підключено |
| Не працюють посилання в PDF | Перевірити renderAnnotationLayer=true та AnnotationLayer.css |
Що входить в роботу
- Аудит поточної реалізації (якщо є)
- Інтеграція react-pdf з обраним режимом
- Кастомний тулбар (навігація, масштабування, пошук)
- Налаштування CORS та завантаження через API
- Документація та приклади використання
- Гарантія на код 6 місяців
Обговоріть ваш проект — ми підберемо оптимальний режим під ваш бюджет. Зв'яжіться з нами, щоб отримати попередню оцінку термінів під ключ: від 1 до 4 днів залежно від складності. Замовте інтеграцію з гарантією 6 місяців — економте до 40% бюджету порівняно з самостійною розробкою.







