reportgen
Back to all posts

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 total

The 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 server

7️⃣ Use the Right Templating Engine

The Problem: Some engines are faster than others.

Performance ranking (fastest to slowest):

  1. Raw HTML - No templating overhead
  2. Handlebars - Minimal logic, fast compilation
  3. EJS - JavaScript execution adds overhead
  4. 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:

  1. Switched to async generation
  2. Optimized images (5MB → 800KB per PDF)
  3. Inlined CSS and fonts
  4. 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:

  1. Reduced chart resolution (4K → 1080p)
  2. Switched from EJS to Handlebars
  3. Implemented batch processing
  4. 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.