Comparing PNG optimizers in Node.js
Optimizing PNG images is a critical step for enhancing web performance. With large image files
potentially slowing down websites, developers have several tools at their disposal to compress PNGs
effectively. In this post, we compare three popular PNG optimization tools that integrate well with
Node.js: pngquant, OptiPNG, and sharp.
Tool overview
Pngquant
pngquant is a command-line utility that applies lossy compression to PNG images.
It can significantly reduce file sizes while maintaining near-original visual quality. Available for
Linux, macOS, and Windows, it's a favorite in many image processing pipelines.
Optipng
OptiPNG is a lossless optimizer for PNG files. It recompresses image data without
any deterioration in quality. Although it may not achieve the aggressive file size reduction seen
with pngquant, it is ideal for scenarios where preserving every pixel matters.
Sharp
Sharp is a high-performance Node.js library for image processing, including PNG
optimization. Instead of calling external command-line tools, sharp runs directly in your Node.js
environment, offering a unified API for cropping, resizing, and compressing images.
Installation
Use Node.js 22.12 or later. Install OptiPNG with your platform’s package manager and the Node.js
packages below. pngquant-bin supplies its own pngquant executable, whose version can differ
from your system installation:
# Ubuntu/Debian
apt-get install pngquant optipng
# macOS
brew install pngquant optipng
# Node.js
npm install sharp@0.35 pngquant-bin@9 pretty-bytes@7
Integrating tools into your Node.js workflow
Put the following JavaScript blocks in one .mjs file. They share imports and helper functions.
Use distinct input and output paths: these examples overwrite their named output files on repeat
runs, so choose paths dedicated to generated images.
Using Pngquant via child processes
Use Node.js's execFile with pngquant-bin for better error handling and cross-platform support:
import { execFile } from 'node:child_process'
import pngquant from 'pngquant-bin'
function optimizeWithPngquant(inputFile, outputFile) {
console.time('pngquant-optimization')
return new Promise((resolve, reject) => {
execFile(
pngquant,
['--quality=65-80', '--strip', '--speed=3', '--force', '--output', outputFile, '--', inputFile],
(error) => {
console.timeEnd('pngquant-optimization')
if (error?.code === 99) {
// pngquant writes no output when it cannot meet the minimum quality.
resolve(inputFile)
return
}
if (error) {
reject(new Error('Pngquant optimization failed', { cause: error }))
return
}
resolve(outputFile)
},
)
})
}
pngquant’s quality setting can cause exit code 99 when the minimum quality cannot be met. In that case the function returns the original input path; any output from an earlier run is not included in this run’s comparison.
Using Optipng via child processes
Implement OptiPNG with proper error handling and performance tracking:
import { promisify } from 'node:util'
const execFilePromise = promisify(execFile)
async function optimizeWithOptipng(inputFile, outputFile) {
console.time('optipng-optimization')
try {
await execFilePromise('optipng', [
'-o7', '-clobber', '-strip', 'all', '-out', outputFile, '--', inputFile,
])
console.timeEnd('optipng-optimization')
return outputFile
} catch (error) {
console.timeEnd('optipng-optimization')
throw new Error('OptiPNG optimization failed', { cause: error })
}
}
Using sharp for in-process optimization
This Sharp example produces a lossy palette PNG, comparable to pngquant. For lossless compression,
omit quality, palette, and effort and retain compressionLevel: 9. Compare results on your own
images; optimization does not guarantee a smaller file:
import sharp from 'sharp'
async function optimizeWithSharp(inputFile, outputFile) {
console.time('sharp-optimization')
try {
await sharp(inputFile)
.png({
quality: 80,
compressionLevel: 9,
palette: true,
effort: 10,
})
.toFile(outputFile)
console.timeEnd('sharp-optimization')
return outputFile
} catch (error) {
console.timeEnd('sharp-optimization')
throw new Error('Sharp optimization failed', { cause: error })
}
}
File size comparison
Use this robust comparison utility to evaluate optimization results:
import fs from 'node:fs'
import prettyBytes from 'pretty-bytes'
function compareFiles(files) {
const results = files.map((file) => {
try {
const stats = fs.statSync(file)
return {
file: file,
size: stats.size,
prettySize: prettyBytes(stats.size),
}
} catch (error) {
return {
file: file,
error: `Could not read file: ${error.message}`,
}
}
})
return results.sort((a, b) => (a.size ?? Infinity) - (b.size ?? Infinity))
}
async function compareOptimizations(inputFile) {
const results = await Promise.all([
optimizeWithPngquant(inputFile, 'output-pngquant.png'),
optimizeWithOptipng(inputFile, 'output-optipng.png'),
optimizeWithSharp(inputFile, 'output-sharp.png'),
])
const comparison = compareFiles([...new Set([inputFile, ...results])])
console.table(comparison)
}
compareOptimizations('input.png').catch((error) => {
console.error(error.message)
process.exitCode = 1
})
Best practices for optimization
When implementing image optimization in your workflow:
- Use worker threads for batch processing large numbers of images
- Implement proper error handling and retries
- Monitor memory usage when processing large images
- Consider implementing progressive loading for web delivery
- Evaluate WebP as a modern alternative to PNG
- Implement proper cleanup of temporary files
- Add logging and monitoring for production environments
Choosing the right tool
Each optimizer offers distinct advantages:
pngquant: Best for aggressive lossy compression with minimal visual impact- OptiPNG: Ideal for lossless optimization where quality preservation is crucial
sharp: Excellent for integrated image processing workflows with good performance
Consider factors like image quality requirements, processing speed, and integration complexity when selecting a solution for your projects.
Conclusion
PNG optimization is crucial for web performance. Modern tools like pngquant, OptiPNG, and sharp
provide powerful options for implementing efficient image optimization in Node.js applications. Each
tool offers unique benefits, and testing them with your specific image assets will help determine
the optimal solution for your needs.
Transloadit uses pngquant in its /image/optimize Robot to ensure
efficient image processing at scale.
