Skip to content

Tối ưu hiệu suất

Sharp đã là một thư viện xử lý hình ảnh hiệu suất cao, nhưng thông qua một số kỹ thuật tối ưu, bạn có thể cải thiện hiệu suất hơn nữa.

Kiểm tra điểm chuẩn

Lợi thế hiệu suất của Sharp so với các thư viện xử lý hình ảnh khác:

  • Nhanh hơn ImageMagick 4-5 lần
  • Nhanh hơn GraphicsMagick 4-5 lần
  • Chiếm dụng bộ nhớ thấp hơn
  • Hỗ trợ xử lý luồng

Tối ưu bộ nhớ

Sử dụng xử lý luồng

Đối với tệp lớn, sử dụng xử lý luồng có thể giảm đáng kể chiếm dụng bộ nhớ:

javascript
import fs from 'fs';

// ❌ Không khuyến nghị: Tải toàn bộ tệp vào bộ nhớ
const buffer = fs.readFileSync('large-image.jpg');
await sharp(buffer).resize(800, 600).toFile('output.jpg');

// ✅ Khuyến nghị: Sử dụng xử lý luồng
fs.createReadStream('large-image.jpg')
  .pipe(sharp().resize(800, 600).jpeg())
  .pipe(fs.createWriteStream('output.jpg'));

Xử lý theo khối

Đối với tệp siêu lớn, có thể xử lý theo khối:

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');

Giải phóng bộ nhớ kịp thời

javascript
// Dọn dẹp kịp thời sau khi xử lý xong
async function processImage(inputPath, outputPath) {
  const sharpInstance = sharp(inputPath);
  
  try {
    await sharpInstance
      .resize(800, 600)
      .jpeg({ quality: 80 })
      .toFile(outputPath);
  } finally {
    // Dọn dẹp thủ công (mặc dù Node.js sẽ tự động thu gom rác)
    sharpInstance.destroy();
  }
}

Tối ưu đồng thời

Điều khiển số lượng đồng thời

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);

Sử dụng Worker thread

Đối với tác vụ CPU-intensive, có thể sử dụng Worker thread:

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

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

Tối ưu bộ nhớ đệm

Bộ nhớ đệm kết quả xử lý

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('Sử dụng kết quả bộ nhớ đệm');
      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 });

Tối ưu thuật toán

Chọn thuật toán điều chỉnh phù hợp

javascript
// Đối với ảnh, sử dụng kernel lanczos3
await sharp('photo.jpg')
  .resize(800, 600, { kernel: sharp.kernel.lanczos3 })
  .toFile('photo-resized.jpg');

// Đối với biểu tượng hoặc hình vẽ đường, sử dụng kernel nearest
await sharp('icon.png')
  .resize(32, 32, { kernel: sharp.kernel.nearest })
  .toFile('icon-resized.png');

// Đối với hình ảnh cần xử lý nhanh, sử dụng kernel cubic
await sharp('image.jpg')
  .resize(300, 200, { kernel: sharp.kernel.cubic })
  .toFile('image-resized.jpg');

Tối ưu chất lượng JPEG

javascript
// Điều chỉnh chất lượng theo nội dung hình ảnh
async function optimizeJPEGQuality(inputPath, outputPath) {
  const metadata = await sharp(inputPath).metadata();
  
  // Điều chỉnh chất lượng theo kích thước hình ảnh
  let quality = 80;
  if (metadata.width > 1920 || metadata.height > 1080) {
    quality = 85; // Hình ảnh lớn sử dụng chất lượng cao hơn
  } else if (metadata.width < 800 && metadata.height < 600) {
    quality = 75; // Hình ảnh nhỏ có thể sử dụng chất lượng thấp hơn
  }
  
  await sharp(inputPath)
    .jpeg({ 
      quality,
      progressive: true, // JPEG lũy tiến
      mozjpeg: true      // Sử dụng tối ưu mozjpeg
    })
    .toFile(outputPath);
}

Tối ưu mạng

Tạo trước các kích thước khác nhau

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');

Sử dụng định dạng hiện đại

javascript
// Tạo nhiều định dạng để hỗ trợ các trình duyệt khác nhau
async function generateModernFormats(inputPath) {
  const promises = [
    // JPEG làm dự phòng
    sharp(inputPath)
      .resize(800, 600)
      .jpeg({ quality: 80 })
      .toFile('output.jpg'),
    
    // WebP cho trình duyệt hiện đại
    sharp(inputPath)
      .resize(800, 600)
      .webp({ quality: 80 })
      .toFile('output.webp'),
    
    // AVIF cho trình duyệt mới nhất
    sharp(inputPath)
      .resize(800, 600)
      .avif({ quality: 80 })
      .toFile('output.avif')
  ];
  
  await Promise.all(promises);
}

Giám sát và gỡ lỗi

Giám sát hiệu suất

javascript
import { performance } from 'perf_hooks';

async function measurePerformance(fn) {
  const start = performance.now();
  const result = await fn();
  const end = performance.now();
  
  console.log(`Thời gian thực thi: ${end - start}ms`);
  return result;
}

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

Giám sát sử dụng bộ nhớ

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('Sử dụng bộ nhớ trước khi xử lý:', getMemoryUsage());

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

console.log('Sử dụng bộ nhớ sau khi xử lý:', getMemoryUsage());

Tóm tắt thực hành tốt nhất

  1. Sử dụng xử lý luồng cho tệp lớn
  2. Điều khiển số lượng đồng thời
  3. Bộ nhớ đệm kết quả xử lý
  4. Chọn thuật toán điều chỉnh phù hợp
  5. Tối ưu định dạng và chất lượng đầu ra
  6. Tạo trước các kích thước thường dùng
  7. Giám sát các chỉ số hiệu suất

So sánh hiệu suất

Thao tácSharpImageMagickGraphicsMagick
Thay đổi kích thước100ms450ms420ms
Chuyển đổi định dạng80ms380ms360ms
Áp dụng bộ lọc120ms520ms480ms
Chiếm dụng bộ nhớThấpCaoCao

Bước tiếp theo

Được phát hành theo Giấy phép Apache 2.0.