Skip to content

Otimização de Performance

Sharp já é uma biblioteca de processamento de imagem de alto desempenho, mas através de algumas técnicas de otimização, você pode melhorar ainda mais a performance.

Benchmark

Vantagens de performance do Sharp em comparação com outras bibliotecas de processamento de imagem:

  • 4-5 vezes mais rápido que ImageMagick
  • 4-5 vezes mais rápido que GraphicsMagick
  • Menor uso de memória
  • Suporta processamento em stream

Otimização de Memória

Usar Processamento em Stream

Para arquivos grandes, usar processamento em stream pode reduzir significativamente o uso de memória:

javascript
import fs from 'fs';

// ❌ Não recomendado: carregar todo o arquivo na memória
const buffer = fs.readFileSync('large-image.jpg');
await sharp(buffer).resize(800, 600).toFile('output.jpg');

// ✅ Recomendado: usar processamento em stream
fs.createReadStream('large-image.jpg')
  .pipe(sharp().resize(800, 600).jpeg())
  .pipe(fs.createWriteStream('output.jpg'));

Processamento em Blocos

Para arquivos muito grandes, pode-se processar em blocos:

javascript
import fs from 'fs';

async function processLargeFile(inputPath, outputPath, chunkSize = 1024 * 1024) {
  const pipeline = sharp()
    .resize(800, 600)
    .jpeg({ quality: 80 });

  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(inputPath, { highWaterMark: chunkSize });
    const writeStream = fs.createWriteStream(outputPath);

    readStream
      .pipe(pipeline)
      .pipe(writeStream)
      .on('finish', resolve)
      .on('error', reject);
  });
}

await processLargeFile('large-input.jpg', 'output.jpg');

Liberar Memória Prontamente

javascript
// Limpar prontamente após processamento
async function processImage(inputPath, outputPath) {
  const sharpInstance = sharp(inputPath);
  
  try {
    await sharpInstance
      .resize(800, 600)
      .jpeg({ quality: 80 })
      .toFile(outputPath);
  } finally {
    // Limpar manualmente (embora Node.js faça garbage collection automaticamente)
    sharpInstance.destroy();
  }
}

Otimização de Concorrência

Controlar Número de Concorrência

javascript
async function processWithConcurrency(files, concurrency = 3) {
  const results = [];
  
  for (let i = 0; i < files.length; i += concurrency) {
    const batch = files.slice(i, i + concurrency);
    const batchPromises = batch.map(file => 
      sharp(file)
        .resize(300, 200)
        .jpeg({ quality: 80 })
        .toFile(`processed-${file}`)
    );
    
    const batchResults = await Promise.all(batchPromises);
    results.push(...batchResults);
  }
  
  return results;
}

const files = ['file1.jpg', 'file2.jpg', 'file3.jpg', 'file4.jpg'];
await processWithConcurrency(files, 2);

Usar Threads Worker

Para tarefas intensivas em CPU, pode-se usar threads Worker:

javascript
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';

if (isMainThread) {
  // Thread principal
  async function processWithWorkers(files, numWorkers = 4) {
    const workers = [];
    const results = [];
    
    for (let i = 0; i < numWorkers; i++) {
      const worker = new Worker('./image-worker.js', {
        workerData: { files: files.slice(i * Math.ceil(files.length / numWorkers), (i + 1) * Math.ceil(files.length / numWorkers)) }
      });
      
      worker.on('message', (result) => {
        results.push(result);
      });
      
      workers.push(worker);
    }
    
    await Promise.all(workers.map(worker => new Promise(resolve => worker.on('exit', resolve))));
    return results;
  }
  
  const files = ['file1.jpg', 'file2.jpg', 'file3.jpg'];
  await processWithWorkers(files);
} else {
  // Thread Worker
  const { files } = workerData;
  
  for (const file of files) {
    await sharp(file)
      .resize(300, 200)
      .jpeg({ quality: 80 })
      .toFile(`processed-${file}`);
  }
  
  parentPort.postMessage('done');
}

Otimização de Cache

Cachear Resultados de Processamento

javascript
import crypto from 'crypto';
import fs from 'fs';

class ImageCache {
  constructor(cacheDir = './cache') {
    this.cacheDir = cacheDir;
    if (!fs.existsSync(cacheDir)) {
      fs.mkdirSync(cacheDir, { recursive: true });
    }
  }

  generateCacheKey(inputPath, options) {
    const content = JSON.stringify({ inputPath, options });
    return crypto.createHash('md5').update(content).digest('hex');
  }

  async getCachedResult(cacheKey) {
    const cachePath = `${this.cacheDir}/${cacheKey}.jpg`;
    if (fs.existsSync(cachePath)) {
      return cachePath;
    }
    return null;
  }

  async setCachedResult(cacheKey, resultPath) {
    const cachePath = `${this.cacheDir}/${cacheKey}.jpg`;
    fs.copyFileSync(resultPath, cachePath);
  }

  async processImage(inputPath, options) {
    const cacheKey = this.generateCacheKey(inputPath, options);
    const cached = await this.getCachedResult(cacheKey);
    
    if (cached) {
      console.log('Usar resultado em cache');
      return cached;
    }

    const outputPath = `output-${Date.now()}.jpg`;
    await sharp(inputPath)
      .resize(options.width, options.height)
      .jpeg({ quality: options.quality })
      .toFile(outputPath);

    await this.setCachedResult(cacheKey, outputPath);
    return outputPath;
  }
}

const cache = new ImageCache();
await cache.processImage('input.jpg', { width: 300, height: 200, quality: 80 });

Otimização de Algoritmo

Escolher Algoritmo de Redimensionamento Apropriado

javascript
// Para fotos, usar kernel lanczos3
await sharp('photo.jpg')
  .resize(800, 600, { kernel: sharp.kernel.lanczos3 })
  .toFile('photo-resized.jpg');

// Para ícones ou imagens de linha, usar kernel nearest
await sharp('icon.png')
  .resize(32, 32, { kernel: sharp.kernel.nearest })
  .toFile('icon-resized.png');

// Para imagens que precisam de processamento rápido, usar kernel cubic
await sharp('image.jpg')
  .resize(300, 200, { kernel: sharp.kernel.cubic })
  .toFile('image-resized.jpg');

Otimizar Qualidade JPEG

javascript
// Ajustar qualidade de acordo com conteúdo da imagem
async function optimizeJPEGQuality(inputPath, outputPath) {
  const metadata = await sharp(inputPath).metadata();
  
  // Ajustar qualidade de acordo com dimensões da imagem
  let quality = 80;
  if (metadata.width > 1920 || metadata.height > 1080) {
    quality = 85; // Imagens grandes usam qualidade mais alta
  } else if (metadata.width < 800 && metadata.height < 600) {
    quality = 75; // Imagens pequenas podem usar qualidade mais baixa
  }
  
  await sharp(inputPath)
    .jpeg({ 
      quality,
      progressive: true, // JPEG progressivo
      mozjpeg: true      // Usar otimização mozjpeg
    })
    .toFile(outputPath);
}

Otimização de Rede

Pré-gerar Diferentes Tamanhos

javascript
const sizes = [
  { width: 320, suffix: 'sm' },
  { width: 640, suffix: 'md' },
  { width: 1024, suffix: 'lg' },
  { width: 1920, suffix: 'xl' }
];

async function pregenerateSizes(inputPath) {
  const promises = sizes.map(size => 
    sharp(inputPath)
      .resize(size.width, null, { fit: 'inside' })
      .jpeg({ quality: 80 })
      .toFile(`output-${size.suffix}.jpg`)
  );
  
  await Promise.all(promises);
}

await pregenerateSizes('input.jpg');

Usar Formatos Modernos

javascript
// Gerar múltiplos formatos para suportar diferentes navegadores
async function generateModernFormats(inputPath) {
  const promises = [
    // JPEG como fallback
    sharp(inputPath)
      .resize(800, 600)
      .jpeg({ quality: 80 })
      .toFile('output.jpg'),
    
    // WebP para navegadores modernos
    sharp(inputPath)
      .resize(800, 600)
      .webp({ quality: 80 })
      .toFile('output.webp'),
    
    // AVIF para navegadores mais recentes
    sharp(inputPath)
      .resize(800, 600)
      .avif({ quality: 80 })
      .toFile('output.avif')
  ];
  
  await Promise.all(promises);
}

Monitoramento e Depuração

Monitoramento de Performance

javascript
import { performance } from 'perf_hooks';

async function measurePerformance(fn) {
  const start = performance.now();
  const result = await fn();
  const end = performance.now();
  
  console.log(`Tempo de execução: ${end - start}ms`);
  return result;
}

await measurePerformance(async () => {
  await sharp('input.jpg')
    .resize(800, 600)
    .jpeg({ quality: 80 })
    .toFile('output.jpg');
});

Monitoramento de Uso de Memória

javascript
import { performance } from 'perf_hooks';

function getMemoryUsage() {
  const usage = process.memoryUsage();
  return {
    rss: `${Math.round(usage.rss / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`,
    heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
    external: `${Math.round(usage.external / 1024 / 1024)}MB`
  };
}

console.log('Uso de memória antes do processamento:', getMemoryUsage());

await sharp('input.jpg')
  .resize(800, 600)
  .jpeg({ quality: 80 })
  .toFile('output.jpg');

console.log('Uso de memória após processamento:', getMemoryUsage());

Resumo de Melhores Práticas

  1. Usar processamento em stream para arquivos grandes
  2. Controlar número de concorrência
  3. Cachear resultados de processamento
  4. Escolher algoritmo de redimensionamento apropriado
  5. Otimizar formato e qualidade de saída
  6. Pré-gerar tamanhos comuns
  7. Monitorar indicadores de performance

Comparação de Performance

OperaçãoSharpImageMagickGraphicsMagick
Redimensionamento100ms450ms420ms
Conversão de formato80ms380ms360ms
Aplicação de filtro120ms520ms480ms
Uso de memóriaBaixoAltoAlto

Próximos Passos

Lançado sob a Licença Apache 2.0.