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:
- Complexidade do Template: HTML/CSS complexo demora mais para renderizar
- Tamanho dos Dados: Grandes volumes de dados aumentam tempo de processamento
- Imagens e Assets: Downloads e processamento de recursos externos
- Network Latency: Tempo de comunicação com a API
- 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
- Implemente Caching para máxima performance
- Revise Best Practices para código de qualidade
- Confira Security para proteção de dados