Guides

Performance

Otimize a geração de PDFs e reduza custos com estratégias de performance

Este guia apresenta técnicas para otimizar a performance da geração de PDFs, reduzir latência e minimizar custos de API.

Entendendo Performance

Fatores que Afetam Performance

A geração de PDFs envolve várias etapas que impactam a performance:

  1. Complexidade do Template: HTML/CSS complexo demora mais para renderizar
  2. Tamanho dos Dados: Grandes volumes de dados aumentam tempo de processamento
  3. Imagens e Assets: Downloads e processamento de recursos externos
  4. Network Latency: Tempo de comunicação com a API
  5. Processamento do Servidor: Carga atual dos servidores RenderHub

Métricas Importantes

interface PerformanceMetrics {
  // Tempo total de ponta a ponta
  totalDuration: number;

  // Tempo de preparação de dados
  dataPreparationTime: number;

  // Tempo de chamada à API
  apiCallTime: number;

  // Tempo de renderização (server-side)
  renderTime: number;

  // Tamanho do PDF gerado
  pdfSize: number;

  // Taxa de sucesso
  successRate: number;
}

Otimização de Templates

Simplifique o HTML

Templates mais simples renderizam mais rápido:

<!-- ❌ Evite: HTML complexo desnecessário -->
<div class="container">
  <div class="wrapper">
    <div class="inner">
      <div class="content">
        <div class="text-holder">
          <span class="label">{{text}}</span>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- ✅ Recomendado: HTML simplificado -->
<div class="content">
  <span>{{text}}</span>
</div>

Otimize CSS

CSS eficiente reduz tempo de renderização:

/* ❌ Evite: Seletores complexos */
div > ul > li > a:hover > span.icon::before {
  content: '→';
}

/* ✅ Recomendado: Seletores simples */
.link-icon::before {
  content: '→';
}

/* ❌ Evite: Propriedades custosas */
.box {
  box-shadow: 0 0 50px rgba(0,0,0,0.5),
              0 0 100px rgba(0,0,0,0.3),
              inset 0 0 20px rgba(255,255,255,0.2);
  filter: blur(5px) brightness(1.2) contrast(1.5);
}

/* ✅ Recomendado: Efeitos simples */
.box {
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

Evite Web Fonts Pesadas

Fontes externas aumentam tempo de carregamento:

<!-- ❌ Evite: Múltiplas variações de fontes -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;200;300;400;500;600;700;800;900&display=swap">

<!-- ✅ Recomendado: Apenas pesos necessários -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap">

<!-- ✅ Melhor ainda: Fontes do sistema -->
<style>
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  }
</style>

Otimize Imagens

Imagens são o maior gargalo de performance:

<!-- ❌ Evite: Imagens não otimizadas -->
<img src="https://example.com/logo.png" width="200">

<!-- ✅ Recomendado: Imagens otimizadas -->
<img src="https://cdn.example.com/logo-200w.webp"
     width="200"
     alt="Logo"
     loading="lazy">

Dicas para imagens:

  • Use WebP ou JPEG otimizado
  • Redimensione para o tamanho exato necessário
  • Comprima com ferramentas como TinyPNG
  • Use CDN para servir imagens
  • Considere usar data URIs para imagens pequenas
// ✅ Imagens pequenas como data URI
const logoDataUri = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIi...';

const data = {
  logo: logoDataUri, // Sem requisição HTTP adicional
  companyName: 'Acme Corp',
};

Otimização de Dados

Envie Apenas Dados Necessários

Reduza o payload da requisição:

// ❌ Evite: Enviando objeto completo
const invoice = await db.invoices.findById(id, {
  include: ['customer', 'items', 'payments', 'history', 'attachments'],
});

const pdf = await renderHub.generatePDF(templateId, invoice);

// ✅ Recomendado: Apenas campos necessários
const invoice = await db.invoices.findById(id, {
  select: ['id', 'number', 'date', 'total'],
  include: {
    customer: { select: ['name', 'email'] },
    items: { select: ['name', 'quantity', 'price'] },
  },
});

const templateData = {
  invoiceNumber: invoice.number,
  invoiceDate: invoice.date,
  customerName: invoice.customer.name,
  items: invoice.items,
  total: invoice.total,
};

const pdf = await renderHub.generatePDF(templateId, templateData);

Pré-processe Cálculos

Faça cálculos antes de enviar para o template:

// ❌ Evite: Cálculos no template
const data = {
  items: [
    { name: 'Item 1', price: 100, quantity: 2 },
    { name: 'Item 2', price: 50, quantity: 3 },
  ],
};

// Template precisa calcular subtotais e total
{{#each items}}
  Subtotal: {{multiply price quantity}}
{{/each}}
Total: {{sum items}}

// ✅ Recomendado: Cálculos feitos no servidor
const data = {
  items: items.map(item => ({
    name: item.name,
    price: item.price,
    quantity: item.quantity,
    subtotal: item.price * item.quantity, // Pré-calculado
  })),
  total: items.reduce((sum, item) => sum + (item.price * item.quantity), 0),
};

// Template apenas exibe valores
{{#each items}}
  Subtotal: {{subtotal}}
{{/each}}
Total: {{total}}

Limite Arrays Grandes

Arrays muito grandes impactam performance:

// ❌ Evite: Enviar todos os registros
const data = {
  transactions: await db.transactions.findAll({ userId }), // 10,000 registros
};

// ✅ Recomendado: Pagine ou limite
const data = {
  transactions: await db.transactions.findAll({
    where: { userId },
    limit: 100, // Máximo por página
    order: [['date', 'DESC']],
  }),
  hasMore: totalTransactions > 100,
};

Se precisar de muitos registros, considere criar múltiplos PDFs:

// ✅ Melhor: Dividir em múltiplos PDFs
async function generateTransactionReport(userId: string) {
  const pageSize = 100;
  const transactions = await db.transactions.findAll({ userId });
  const totalPages = Math.ceil(transactions.length / pageSize);

  const pdfPromises = [];

  for (let page = 0; page < totalPages; page++) {
    const pageTransactions = transactions.slice(
      page * pageSize,
      (page + 1) * pageSize
    );

    pdfPromises.push(
      renderHub.generatePDF(templateId, {
        transactions: pageTransactions,
        page: page + 1,
        totalPages,
      })
    );
  }

  return await Promise.all(pdfPromises);
}

Processamento Paralelo

Promise.all para Múltiplos PDFs

Gere múltiplos PDFs em paralelo:

// ❌ Evite: Processamento sequencial (lento)
async function generateInvoices(invoiceIds: string[]) {
  const pdfs = [];

  for (const id of invoiceIds) {
    const data = await getInvoiceData(id);
    const pdf = await renderHub.generatePDF(templateId, data);
    pdfs.push(pdf);
  }

  return pdfs;
}
// Tempo: ~10s para 10 invoices (1s cada)

// ✅ Recomendado: Processamento paralelo (rápido)
async function generateInvoices(invoiceIds: string[]) {
  const pdfPromises = invoiceIds.map(async (id) => {
    const data = await getInvoiceData(id);
    return renderHub.generatePDF(templateId, data);
  });

  return await Promise.all(pdfPromises);
}
// Tempo: ~1.5s para 10 invoices (paralelo)

Controle de Concorrência

Evite sobrecarregar a API com muitas requisições simultâneas:

// ✅ Processamento com limite de concorrência
async function generateInvoicesWithLimit(
  invoiceIds: string[],
  concurrency = 5
) {
  const results = [];

  for (let i = 0; i < invoiceIds.length; i += concurrency) {
    const batch = invoiceIds.slice(i, i + concurrency);

    const batchResults = await Promise.all(
      batch.map(async (id) => {
        const data = await getInvoiceData(id);
        return renderHub.generatePDF(templateId, data);
      })
    );

    results.push(...batchResults);
  }

  return results;
}

Ou use bibliotecas especializadas:

import pLimit from 'p-limit';

async function generateInvoicesWithLimit(invoiceIds: string[]) {
  const limit = pLimit(5); // Máximo 5 requisições simultâneas

  const pdfPromises = invoiceIds.map(id =>
    limit(async () => {
      const data = await getInvoiceData(id);
      return renderHub.generatePDF(templateId, data);
    })
  );

  return await Promise.all(pdfPromises);
}

Connection Pooling

Reutilize Conexões HTTP

Mantenha conexões HTTP abertas para requisições subsequentes:

import { RenderHubClient } from '@renderhub/sdk';

// ❌ Evite: Criar novo cliente a cada requisição
export async function generatePDF(data: any) {
  const client = new RenderHubClient({ apiKey: process.env.API_KEY });
  return await client.generatePDF(templateId, data);
}

// ✅ Recomendado: Reutilizar cliente (singleton)
const renderHubClient = new RenderHubClient({
  apiKey: process.env.RENDERHUB_API_KEY!,
  keepAlive: true, // Manter conexões abertas
  maxSockets: 50, // Pool de conexões
});

export async function generatePDF(data: any) {
  return await renderHubClient.generatePDF(templateId, data);
}

Streaming e Processamento Assíncrono

Stream de PDFs Grandes

Para PDFs muito grandes, use streaming:

import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

// ✅ Stream direto para arquivo
async function generateAndSavePDF(data: any, outputPath: string) {
  const pdfStream = await renderHub.generatePDFStream(templateId, data);
  const fileStream = createWriteStream(outputPath);

  await pipeline(pdfStream, fileStream);
}

// ✅ Stream direto para resposta HTTP
app.get('/invoice/:id/pdf', async (req, res) => {
  const data = await getInvoiceData(req.params.id);
  const pdfStream = await renderHub.generatePDFStream(templateId, data);

  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Disposition', 'attachment; filename="invoice.pdf"');

  pdfStream.pipe(res);
});

Processamento em Background

Para tarefas pesadas, use filas:

import { Queue, Worker } from 'bullmq';

// Definir fila
const pdfQueue = new Queue('pdf-generation', {
  connection: { host: 'localhost', port: 6379 },
});

// Adicionar job à fila
export async function queuePDFGeneration(invoiceId: string) {
  await pdfQueue.add('generate-invoice', {
    invoiceId,
    templateId: 'invoice-v2',
  });

  return { status: 'queued', invoiceId };
}

// Worker para processar jobs
const worker = new Worker(
  'pdf-generation',
  async (job) => {
    const { invoiceId, templateId } = job.data;

    // Obter dados
    const data = await getInvoiceData(invoiceId);

    // Gerar PDF
    const pdf = await renderHub.generatePDF(templateId, data);

    // Salvar em storage
    await uploadToS3(pdf, `invoices/${invoiceId}.pdf`);

    // Notificar usuário
    await sendEmail(data.customerEmail, {
      subject: 'Sua fatura está pronta',
      pdfUrl: `https://cdn.example.com/invoices/${invoiceId}.pdf`,
    });

    return { success: true, invoiceId };
  },
  { connection: { host: 'localhost', port: 6379 } }
);

Compressão

Comprima PDFs Gerados

Reduza tamanho de PDFs para armazenamento e transferência:

import { compress } from 'pdf-compressor';

// ✅ Comprimir PDF
async function generateCompressedPDF(data: any) {
  const pdf = await renderHub.generatePDF(templateId, data);

  // Comprimir (reduz 30-50% do tamanho)
  const compressedPdf = await compress(pdf, {
    quality: 85, // 0-100, quanto menor mais compressão
    optimizeImages: true,
  });

  return compressedPdf;
}

Comprima Requisições HTTP

Use compressão gzip/brotli para requisições:

import { RenderHubClient } from '@renderhub/sdk';

const client = new RenderHubClient({
  apiKey: process.env.RENDERHUB_API_KEY!,
  compression: true, // Habilitar compressão gzip
});

Monitoramento de Performance

Instrumentação

Meça performance de cada etapa:

import { performance } from 'perf_hooks';

async function generatePDFWithMetrics(data: any) {
  const metrics: PerformanceMetrics = {} as any;

  // Preparação de dados
  const startDataPrep = performance.now();
  const templateData = prepareData(data);
  metrics.dataPreparationTime = performance.now() - startDataPrep;

  // Chamada à API
  const startApi = performance.now();
  const pdf = await renderHub.generatePDF(templateId, templateData);
  metrics.apiCallTime = performance.now() - startApi;

  // Métricas adicionais
  metrics.pdfSize = pdf.length;
  metrics.totalDuration = metrics.dataPreparationTime + metrics.apiCallTime;

  // Log métricas
  logger.info('PDF gerado', {
    templateId,
    metrics,
  });

  return pdf;
}

Alertas de Performance

Configure alertas para degradação:

import { Counter, Histogram } from 'prom-client';

const pdfDuration = new Histogram({
  name: 'pdf_generation_duration_seconds',
  help: 'Duração da geração de PDF',
  labelNames: ['template'],
  buckets: [0.5, 1, 2, 5, 10, 30], // SLA: 95% < 5s
});

async function generatePDF(data: any) {
  const start = performance.now();

  try {
    const pdf = await renderHub.generatePDF(templateId, data);

    const duration = (performance.now() - start) / 1000;
    pdfDuration.observe({ template: templateName }, duration);

    // Alertar se muito lento
    if (duration > 10) {
      logger.warn('PDF generation slow', {
        duration,
        templateId,
        threshold: 10,
      });
    }

    return pdf;
  } catch (error) {
    const duration = (performance.now() - start) / 1000;
    pdfDuration.observe({ template: templateName }, duration);
    throw error;
  }
}

Benchmarking

Compare Diferentes Abordagens

import { Suite } from 'benchmark';

const suite = new Suite();

suite
  .add('Sequential processing', {
    defer: true,
    fn: async (deferred: any) => {
      await generateInvoicesSequential(invoiceIds);
      deferred.resolve();
    },
  })
  .add('Parallel processing', {
    defer: true,
    fn: async (deferred: any) => {
      await generateInvoicesParallel(invoiceIds);
      deferred.resolve();
    },
  })
  .add('Parallel with limit', {
    defer: true,
    fn: async (deferred: any) => {
      await generateInvoicesWithLimit(invoiceIds, 5);
      deferred.resolve();
    },
  })
  .on('cycle', (event: any) => {
    console.log(String(event.target));
  })
  .on('complete', function (this: any) {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run({ async: true });

Otimização de Custos

Minimize Requisições Desnecessárias

// ❌ Evite: Gerar mesmo PDF múltiplas vezes
app.get('/invoice/:id/pdf', async (req, res) => {
  const data = await getInvoiceData(req.params.id);
  const pdf = await renderHub.generatePDF(templateId, data); // Custo: 1 crédito
  res.send(pdf);
});
// Cada request = 1 crédito

// ✅ Recomendado: Gerar uma vez, armazenar
app.get('/invoice/:id/pdf', async (req, res) => {
  const invoiceId = req.params.id;

  // Verificar se já existe
  const existingPdf = await getFromStorage(invoiceId);
  if (existingPdf) {
    return res.send(existingPdf); // Custo: 0 créditos
  }

  // Gerar apenas se não existe
  const data = await getInvoiceData(invoiceId);
  const pdf = await renderHub.generatePDF(templateId, data); // Custo: 1 crédito

  // Armazenar para futuro
  await saveToStorage(invoiceId, pdf);

  res.send(pdf);
});

Use Cache (veja guia de Caching)

Checklist de Otimização

  • Templates HTML/CSS simplificados
  • Imagens otimizadas e comprimidas
  • Fontes web minimizadas ou removidas
  • Dados pré-processados no servidor
  • Arrays grandes paginados ou divididos
  • Processamento paralelo implementado
  • Limite de concorrência configurado
  • Connection pooling habilitado
  • Streaming para PDFs grandes
  • Processamento em background para tarefas pesadas
  • Compressão de PDFs habilitada
  • Métricas de performance coletadas
  • Alertas configurados para SLA
  • Cache implementado (veja guia de Caching)
  • Custos monitorados

Próximos Passos

On this page