Skip to content

Optimización de Rendimiento

Sharp ya es una biblioteca de procesamiento de imágenes de alto rendimiento, pero a través de algunas técnicas de optimización, puede mejorar aún más el rendimiento.

Pruebas de Rendimiento

Ventajas de rendimiento de Sharp en comparación con otras bibliotecas de procesamiento de imágenes:

  • 4-5 veces más rápido que ImageMagick
  • 4-5 veces más rápido que GraphicsMagick
  • Menor uso de memoria
  • Soporta procesamiento por streams

Optimización de Memoria

Usar Procesamiento por Streams

Para archivos grandes, usar procesamiento por streams puede reducir significativamente el uso de memoria:

javascript
import fs from 'fs';

// ❌ No recomendado: cargar todo el archivo en memoria
const buffer = fs.readFileSync('large-image.jpg');
await sharp(buffer).resize(800, 600).toFile('output.jpg');

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

Procesamiento por Bloques

Para archivos muy grandes, puede procesar por bloques:

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 Memoria a Tiempo

javascript
// Limpiar a tiempo después del procesamiento
async function processImage(inputPath, outputPath) {
  const sharpInstance = sharp(inputPath);
  
  try {
    await sharpInstance
      .resize(800, 600)
      .jpeg({ quality: 80 })
      .toFile(outputPath);
  } finally {
    // Limpieza manual (aunque Node.js hará recolección de basura automáticamente)
    sharpInstance.destroy();
  }
}

Optimización de Concurrencia

Controlar Número de Concurrencia

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 Hilos Worker

Para tareas intensivas en CPU, puede usar hilos Worker:

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

if (isMainThread) {
  // Hilo 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 {
  // Hilo Worker
  const { files } = workerData;
  
  for (const file of files) {
    await sharp(file)
      .resize(300, 200)
      .jpeg({ quality: 80 })
      .toFile(`processed-${file}`);
  }
  
  parentPort.postMessage('done');
}

Optimización de Caché

Caché de Resultados de Procesamiento

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 en caché');
      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 });

Optimización de Algoritmos

Seleccionar Algoritmo de Redimensionamiento Apropiado

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

// Para iconos o gráficos de líneas, usar kernel nearest
await sharp('icon.png')
  .resize(32, 32, { kernel: sharp.kernel.nearest })
  .toFile('icon-resized.png');

// Para imágenes que necesitan procesamiento rápido, usar kernel cubic
await sharp('image.jpg')
  .resize(300, 200, { kernel: sharp.kernel.cubic })
  .toFile('image-resized.jpg');

Optimizar Calidad JPEG

javascript
// Ajustar calidad según contenido de imagen
async function optimizeJPEGQuality(inputPath, outputPath) {
  const metadata = await sharp(inputPath).metadata();
  
  // Ajustar calidad según dimensiones de imagen
  let quality = 80;
  if (metadata.width > 1920 || metadata.height > 1080) {
    quality = 85; // Imágenes grandes usar mayor calidad
  } else if (metadata.width < 800 && metadata.height < 600) {
    quality = 75; // Imágenes pequeñas pueden usar menor calidad
  }
  
  await sharp(inputPath)
    .jpeg({ 
      quality,
      progressive: true, // JPEG progresivo
      mozjpeg: true      // Usar optimización mozjpeg
    })
    .toFile(outputPath);
}

Optimización de Red

Pre-generar Diferentes Tamaños

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
// Generar múltiples formatos para soportar diferentes navegadores
async function generateModernFormats(inputPath) {
  const promises = [
    // JPEG como respaldo
    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 más recientes
    sharp(inputPath)
      .resize(800, 600)
      .avif({ quality: 80 })
      .toFile('output.avif')
  ];
  
  await Promise.all(promises);
}

Monitoreo y Depuración

Monitoreo de Rendimiento

javascript
import { performance } from 'perf_hooks';

async function measurePerformance(fn) {
  const start = performance.now();
  const result = await fn();
  const end = performance.now();
  
  console.log(`Tiempo de ejecución: ${end - start}ms`);
  return result;
}

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

Monitoreo de Uso de Memoria

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 memoria antes del procesamiento:', getMemoryUsage());

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

console.log('Uso de memoria después del procesamiento:', getMemoryUsage());

Resumen de Mejores Prácticas

  1. Usar procesamiento por streams para archivos grandes
  2. Controlar número de concurrencia
  3. Caché de resultados de procesamiento
  4. Seleccionar algoritmo de redimensionamiento apropiado
  5. Optimizar formato y calidad de salida
  6. Pre-generar tamaños comunes
  7. Monitorear indicadores de rendimiento

Comparación de Rendimiento

OperaciónSharpImageMagickGraphicsMagick
Redimensionamiento100ms450ms420ms
Conversión de formato80ms380ms360ms
Aplicación de filtros120ms520ms480ms
Uso de memoriaBajoAltoAlto

Próximos Pasos

Liberado bajo la Licencia Apache 2.0.