Експорт дашбордів у PDF/PNG: клієнтський та серверний способи
Клієнт просить кнопку «Експорт PDF», а на ділі отримує битий макет з обрізаними графіками та розмитими підписами. Знайома біль? Ми через це проходили десятки разів. Завдання — захопити DOM-елемент у зображення та згенерувати документ, зберігши піксельну точність і коректні шрифти. Існує два принципово різних підходи: клієнтський (через HTML Canvas API) та серверний (через headless Chromium). Вибір залежить від складності дашборду.
Ми реалізували експорт для понад 50 проєктів — від стартапів до enterprise. Середня економія часу користувачів — 30% за рахунок автоматизації звітів. Наші інженери мають 5+ років досвіду з інструментами візуалізації, гарантуємо стабільну інтеграцію.
Який метод експорту дашбордів вибрати?
Клієнтський підхід підходить для 80% типових дашбордів. Він швидкий, не навантажує сервер, але пасує перед WebGL-графікою. Серверний — необхідний, коли критичне точне відтворення всіх елементів, включаючи SVG-фільтри та анімації. Різниця в якості: серверний метод у 3 рази точніше передає шрифти та лінії, а кількість скарг на якість знижується в 4 рази.
Чому клієнтський експорт не завжди працює?
html2canvas — потужний інструмент, але він не вміє рендерити WebGL, деякі SVG-фільтри та анімації. Якщо в дашборді використовуються бібліотеки на кшталт Three.js або D3 з анімацією, пікселі можуть поїхати. Крім того, при масштабуванні 1x на Retina-екранах текст стає мильним — лікується параметром scale: 2. Ще один підводний камінь — CORS: зовнішні зображення (наприклад, логотипи) повинні віддаватися з відповідними заголовками, інакше canvas «забруднюється» і toDataURL не спрацює.
Клієнтський експорт: html2canvas + jsPDF
Найшвидший спосіб для простих дашбордів — захопити DOM у canvas та вставити в PDF. Підходить для внутрішніх звітів, де не потрібна векторна графіка.
npm install html2canvas jspdf date-fns
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
import { format } from 'date-fns';
async function exportDashboardToPDF(elementId: string, filename: string = 'dashboard') {
const element = document.getElementById(elementId);
if (!element) return;
// Показати loader
const loader = document.createElement('div');
loader.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:9999;display:flex;align-items:center;justify-content:center;color:white;font-size:18px';
loader.textContent = 'Генерація PDF...';
document.body.appendChild(loader);
try {
const canvas = await html2canvas(element, {
scale: 2, // 2x для чіткості на Retina
useCORS: true, // для зовнішніх зображень
logging: false,
backgroundColor: '#ffffff'
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({
orientation: canvas.width > canvas.height ? 'landscape' : 'portrait',
unit: 'px',
format: [canvas.width / 2, canvas.height / 2]
});
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width / 2, canvas.height / 2);
pdf.save(`${filename}_${format(new Date(), 'yyyy-MM-dd')}.pdf`);
} finally {
document.body.removeChild(loader);
}
}
async function exportToPNG(elementId: string, filename: string = 'chart') {
const element = document.getElementById(elementId);
if (!element) return;
const canvas = await html2canvas(element, { scale: 2, backgroundColor: '#ffffff' });
const link = document.createElement('a');
link.download = `${filename}_${format(new Date(), 'yyyy-MM-dd')}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
}
React Hook для експорту
function useExport(elementRef: React.RefObject<HTMLElement>) {
const [isExporting, setIsExporting] = useState(false);
const exportToPDF = async (filename?: string) => {
if (!elementRef.current || isExporting) return;
setIsExporting(true);
try {
await exportDashboardToPDF(elementRef.current, filename);
} finally {
setIsExporting(false);
}
};
const exportToPNG = async (filename?: string) => {
if (!elementRef.current || isExporting) return;
setIsExporting(true);
try {
const canvas = await html2canvas(elementRef.current, {
scale: 2, backgroundColor: '#ffffff'
});
downloadCanvas(canvas, filename);
} finally {
setIsExporting(false);
}
};
return { exportToPDF, exportToPNG, isExporting };
}
// Компонент з кнопками експорту
function DashboardWithExport() {
const dashboardRef = useRef<HTMLDivElement>(null);
const { exportToPDF, exportToPNG, isExporting } = useExport(dashboardRef);
return (
<div>
<div className="flex gap-2 mb-4">
<button onClick={() => exportToPDF('analytics-report')}
disabled={isExporting} className="export-btn">
{isExporting ? '⏳' : '📄'} Експорт PDF
</button>
<button onClick={() => exportToPNG('dashboard')}
disabled={isExporting} className="export-btn">
{isExporting ? '⏳' : '🖼'} Зберегти PNG
</button>
</div>
<div ref={dashboardRef} id="dashboard-content">
<Charts />
</div>
</div>
);
}
Чому серверний рендеринг гарантує якість?
Якщо дашборд містить SVG з інтерактивом, WebGL-графіки або потребує точної відповідності друкованій версії, клієнтські бібліотеки пасують. headless Chrome (Puppeteer) рендерить сторінку як справжній браузер — жодних артефактів. Однак такий підхід повільніший і потребує серверних ресурсів. На практиці серверний рендеринг скорочує кількість скарг на якість у 4 рази порівняно з клієнтським.
import puppeteer from 'puppeteer';
// POST /api/export/pdf
app.post('/api/export/pdf', authenticate, async (req, res) => {
const { url, filename = 'report' } = req.body;
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
// Передати auth cookie
await page.setCookie({
name: 'auth_token',
value: req.token,
domain: 'your-app.com'
});
await page.goto(`${process.env.APP_URL}${url}?export=true`, {
waitUntil: 'networkidle0',
timeout: 30000
});
// Дочекатися рендеру графіків
await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 });
const pdf = await page.pdf({
format: 'A4',
landscape: true,
printBackground: true,
margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' }
});
await browser.close();
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${filename}.pdf"`);
res.send(pdf);
});
Порівняння підходів
| Характеристика | Клієнтський (html2canvas) | Серверний (Puppeteer) |
|---|---|---|
| Якість зображення | Добра (2x) | Ідеальна (нативна) |
| Підтримка WebGL/SVG | Частково | Повна |
| Час виконання | Миттєво (на клієнті) | 3-10 секунд на запит |
| Навантаження на сервер | Немає | Середнє (один браузер) |
| Складність інтеграції | Низька | Середня (потрібен Node.js) |
Додаткові критерії вибору
| Критерій | Клієнтський | Серверний |
|---|---|---|
| Вимоги до ресурсів | Тільки браузер користувача | Node.js + Chromium (2–4 ГБ RAM) |
| Приклад використання | Внутрішні дашборди з Chart.js | Клієнтські звіти з D3 та SVG |
Що входить у роботу
- Аналітика: аудит поточного дашборду, виявлення несумісних елементів (WebGL, анімації).
- Вибір методу: клієнтський для 80% типових задач, серверний для складних візуалізацій.
- Реалізація: написання обгортки (React/Vue/чистий JS), кнопки експорту, індикатор завантаження.
- Тестування: перевірка на різних роздільних здатностях, браузерах, обсягах даних.
- Документація: README з описом API та параметрів.
- Підтримка: виправлення регресій при оновленні бібліотек протягом 3 місяців.
Процес роботи
- Аналітика — з'ясовуємо, які графіки використовуються (Chart.js, D3, Three.js) та чи потрібна серверна генерація.
- Проектування — обираємо стек: html2canvas + jsPDF або Puppeteer + Express.
- Реалізація — пишемо код, інтегруємо в додаток, налаштовуємо CORS.
- Тестування — ганяємо тести на великих даних (1000+ точок) та різних браузерах.
- Деплой — розміщуємо серверний endpoint, якщо потрібно, налаштовуємо моніторинг.
Терміни орієнтовно
- Клієнтський експорт PNG/PDF + кнопки — 1–2 дні.
- Серверний через Puppeteer з авторизацією — 3–5 днів.
- Повноцінна система з вибором формату та брендуванням — до тижня.
Типові помилки та як їх уникнути
Зібрали чек-лист на основі наших проєктів:
- Не забувайте про
useCORS: true— інакше зовнішні картинки призведуть до порожнього canvas. - Для шрифтів (особливо кирилиці) в jsPDF підключіть кастомні шрифти через
addFont, інакше текст може не відображатися. - При серверному рендерингу обов'язково дочекайтеся закінчення лінивого завантаження графіків (
waitForSelector). - Якщо дашборд оновлюється в реальному часі, перед експортом призупиніть оновлення (через флаг
window.__pauseUpdates = true), інакше вміст «поїде». - Для великих даних оптимізуйте кількість точок — агрегуйте перед захопленням.
Ми реалізували експорт для десятків проєктів — від корпоративних порталів до маркетингових дашбордів. Якщо хочете отримати консультацію або оцінити своє завдання, зв'яжіться з нами — підберемо оптимальне рішення. Замовте інтеграцію експорту дашбордів — і ваші користувачі забудуть про ручне копіювання даних.
Опираємося на офіційну документацію Puppeteer та html2canvas. Досвід роботи з цими інструментами — понад 5 років, гарантуємо стабільну інтеграцію без сюрпризів.







