PDF Performance Optimization: From Seconds to Milliseconds ⚡
March 25, 2025
Your PDFs are taking 5 seconds to generate. For a single user? Annoying. For 1,000 concurrent users? Catastrophic.
Slow PDF generation kills app performance. It blocks API responses. It frustrates users. It makes your monitoring dashboards light up like a Christmas tree.
But here's the thing: most PDF performance issues are fixable. We've helped dozens of teams go from multi-second generation times to sub-second responses.
This guide shows you how.
The Performance Problem
Let's be real about what "slow" means:
- < 500ms - Excellent (feels instant)
- 500ms - 2s - Acceptable (users won't complain)
- 2s - 5s - Slow (users notice the wait)
- > 5s - Broken (users think something's wrong)
If you're above 2 seconds consistently, you have a problem. Let's fix it.
Quick Wins: 80% Faster with 20% Effort
1️⃣ Use Async Generation for Non-Critical PDFs
The Problem: Synchronous generation blocks your API response.
// ❌ Blocks the API response for 3+ seconds
app.post('/generate-report', async (req, res) => {
const pdf = await generatePDFSync(reportData);
res.json({ pdf_url: pdf.url });
// User waits 3 seconds for this response
});The Fix: Use async generation, return immediately.
// ✅ Returns in < 100ms
app.post('/generate-report', async (req, res) => {
const jobId = await generatePDFAsync(reportData);
res.json({
job_id: jobId,
status_url: `/pdf-status/${jobId}`
});
// User gets immediate response
});When to use async:
- Reports that don't need to be instant
- Large PDFs (> 10 pages)
- PDFs with complex rendering (charts, images)
When to use sync:
- Invoices needed immediately
- Simple 1-2 page documents
- Real-time exports
2️⃣ Reduce HTML Complexity
The Problem: Your template is doing too much.
Every element adds rendering time:
- 1,000 DOM elements = slow
- 100 images = slow
- Complex nested tables = slow
The Fix: Simplify your HTML.
<!-- ❌ Over-engineered, slow to render -->
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>{{title}}</h3>
</div>
<div class="card-body">
<p>{{content}}</p>
</div>
</div>
</div>
</div>
</div>
<!-- ✅ Simple, renders fast -->
<table width="100%">
<tr>
<td>
<h3>{{title}}</h3>
<p>{{content}}</p>
</td>
</tr>
</table>Rule of thumb: If you wouldn't write it in raw HTML, you probably don't need it in a PDF.
3️⃣ Optimize Images Aggressively
The Problem: Large images slow down rendering and inflate PDF size.
// ❌ 5MB image in your PDF
<img src="https://example.com/high-res-chart.png">The Fix: Resize images before embedding.
// ✅ Resize images server-side
const resizedImage = await sharp(originalImage)
.resize(800, 600, { fit: 'inside' })
.jpeg({ quality: 80 })
.toBuffer();
const base64Image = resizedImage.toString('base64');
const html = `<img src="data:image/jpeg;base64,${base64Image}" width="800">`;Image optimization checklist:
- Resize to display dimensions (don't embed 4K images)
- Compress with 80% quality (usually imperceptible)
- Use JPEG for photos, PNG for graphics/logos
- Consider WebP (supported in modern PDF renderers)
Impact: One team reduced their 10-page report from 15MB to 2MB, cutting generation time by 60%.
Advanced Optimization Strategies
4️⃣ Cache Rendered Templates
The Problem: Re-rendering the same template with similar data wastes resources.
// ❌ Renders from scratch every time
async function generateInvoice(invoiceId) {
const data = await getInvoiceData(invoiceId);
const html = await renderTemplate('invoice.ejs', data);
const pdf = await generatePDF(html);
return pdf;
}The Fix: Cache rendered HTML when data is similar.
// ✅ Cache template rendering
async function generateInvoice(invoiceId) {
const data = await getInvoiceData(invoiceId);
// Create cache key based on template + data structure
const cacheKey = `invoice:${data.template_version}:${hashData(data)}`;
let html = await cache.get(cacheKey);
if (!html) {
html = await renderTemplate('invoice.ejs', data);
await cache.set(cacheKey, html, { ttl: 3600 });
}
const pdf = await generatePDF(html);
return pdf;
}When this works:
- Templates with static sections
- Data that doesn't change frequently
- Reports run multiple times with same params
When this doesn't work:
- Highly dynamic, unique data every time
- Real-time data that must be fresh
5️⃣ Preload and Inline External Resources
The Problem: Fetching external CSS and fonts adds latency.
<!-- ❌ Multiple external requests = slow -->
<link rel="stylesheet" href="https://cdn.example.com/styles.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">The Fix: Inline everything critical.
// Preload and inline resources at build time
const css = await fetch('https://cdn.example.com/styles.css').then(r => r.text());
const fontCSS = await fetch('https://fonts.googleapis.com/css2?family=Roboto&display=swap').then(r => r.text());
const html = `
<style>
${css}
${fontCSS}
</style>
<body>
<!-- Your content -->
</body>
`;Pro tip: Do this once at app startup, not per PDF generation.
// Cache external resources at startup
const INLINED_RESOURCES = {
styles: null,
fonts: null
};
async function initializeResources() {
INLINED_RESOURCES.styles = await fetch('...').then(r => r.text());
INLINED_RESOURCES.fonts = await fetch('...').then(r => r.text());
}
// Call once on app start
await initializeResources();
// Use in templates
const html = `<style>${INLINED_RESOURCES.styles}</style>...`;6️⃣ Batch PDF Generation
The Problem: Generating PDFs one-by-one for bulk operations is inefficient.
// ❌ Sequential generation = slow
for (const invoice of invoices) {
await generatePDF(invoice);
}
// 100 invoices × 1 second = 100 seconds totalThe Fix: Batch with parallel processing.
// ✅ Parallel generation = fast
const pdfPromises = invoices.map(invoice => generatePDF(invoice));
await Promise.all(pdfPromises);
// 100 invoices in ~5 seconds (with reportgen.io's unlimited concurrency)But be smart about batching:
// ✅ Batch with concurrency control
const BATCH_SIZE = 10;
for (let i = 0; i < invoices.length; i += BATCH_SIZE) {
const batch = invoices.slice(i, i + BATCH_SIZE);
await Promise.all(batch.map(invoice => generatePDF(invoice)));
}
// Controlled concurrency, won't overwhelm your server7️⃣ Use the Right Templating Engine
The Problem: Some engines are faster than others.
Performance ranking (fastest to slowest):
- Raw HTML - No templating overhead
- Handlebars - Minimal logic, fast compilation
- EJS - JavaScript execution adds overhead
- GoTempl - Fast, but only for Go apps
Benchmark (1,000 PDFs generated):
- Raw HTML: 45 seconds
- Handlebars: 52 seconds
- EJS: 68 seconds
The Fix: Use the simplest engine that meets your needs.
// If your data is pre-formatted, use raw HTML
const html = `
<h1>${invoice.title}</h1>
<p>Total: ${invoice.total}</p>
`;
// If you need logic, use Handlebars (not EJS)
const html = `
<h1>{{title}}</h1>
{{#if isPaid}}
<p class="paid">PAID</p>
{{else}}
<p class="unpaid">UNPAID</p>
{{/if}}
`;Measuring Performance
You can't optimize what you don't measure.
Add Timing Instrumentation
async function generatePDF(template, data) {
const start = Date.now();
const renderStart = Date.now();
const html = await renderTemplate(template, data);
const renderTime = Date.now() - renderStart;
const pdfStart = Date.now();
const pdf = await reportgen.generate(html);
const pdfTime = Date.now() - pdfStart;
const totalTime = Date.now() - start;
console.log({
renderTime,
pdfTime,
totalTime
});
return pdf;
}Track Key Metrics
- P50 generation time - Median performance
- P95 generation time - Worst-case performance
- PDF size - Impacts delivery time
- Error rate - Failed generations
Set Performance Budgets
const PERFORMANCE_BUDGETS = {
renderTime: 500, // Template rendering should be < 500ms
pdfTime: 2000, // PDF generation should be < 2s
totalTime: 3000 // Total should be < 3s
};
if (totalTime > PERFORMANCE_BUDGETS.totalTime) {
logger.warn('PDF generation exceeded performance budget', {
totalTime,
budget: PERFORMANCE_BUDGETS.totalTime
});
}Real-World Performance Case Studies
Case Study 1: SaaS Invoicing Platform
Before:
- 4.2s average generation time
- 30% timeout rate on large invoices
- Users complaining about slow exports
Changes:
- Switched to async generation
- Optimized images (5MB → 800KB per PDF)
- Inlined CSS and fonts
- Cached rendered templates
After:
- 0.8s average generation time (5x faster)
- 0% timeout rate
- Users happy
Case Study 2: Analytics Dashboard Exports
Before:
- 8.5s generation time for 20-page reports
- Server CPU spiking to 100%
- Could only handle 5 concurrent generations
Changes:
- Reduced chart resolution (4K → 1080p)
- Switched from EJS to Handlebars
- Implemented batch processing
- Used reportgen.io's async endpoint
After:
- 1.2s generation time (7x faster)
- Normal CPU usage
- Handles 100+ concurrent generations
The Ultimate Performance Checklist
- Use async generation for non-critical PDFs
- Simplify HTML structure (avoid div soup)
- Optimize and resize all images
- Cache rendered templates when possible
- Inline critical CSS and fonts
- Batch PDF generation with controlled concurrency
- Use the simplest templating engine that works
- Measure and monitor generation times
- Set performance budgets and alerts
The reportgen.io Advantage
Some optimizations require infrastructure you don't want to build:
✅ Unlimited concurrency - Generate 1,000 PDFs in parallel ✅ Global CDN - Sub-100ms delivery worldwide ✅ Automatic scaling - Handles traffic spikes automatically ✅ No cold starts - Always fast, even at 3am
When "Fast Enough" Is Good Enough
Remember: premature optimization is the root of all evil.
If your PDFs generate in 2 seconds and users don't complain, you're probably fine. Optimization has diminishing returns.
Focus on performance when:
- Users complain about slowness
- You're hitting rate limits or timeouts
- Cost is directly tied to generation time
- You're generating at scale (1,000+ PDFs/hour)
Otherwise, ship features instead.
Final Thoughts
PDF performance is not magic. It's about:
- Reducing complexity
- Eliminating network requests
- Caching intelligently
- Measuring relentlessly
Follow the strategies in this guide, and you'll get 10x faster PDF generation.
Ready for blazing-fast PDFs? Try reportgen.io - built for performance from day one.