sharp
Sharp
High-performance Node.js image processing. 4-5x faster than ImageMagick for resizing JPEG, PNG, WebP, AVIF, and TIFF.
Quick Start
npm install sharp
import sharp from 'sharp';
// Resize and convert
await sharp('input.jpg')
.resize(800, 600)
.toFormat('webp')
.toFile('output.webp');
// From buffer
const buffer = await sharp(inputBuffer)
.resize(400)
.toBuffer();
Resize
// Fixed dimensions (may crop)
await sharp('input.jpg')
.resize(800, 600)
.toFile('output.jpg');
// Fit within dimensions (maintain aspect ratio)
await sharp('input.jpg')
.resize(800, 600, { fit: 'inside' })
.toFile('output.jpg');
// Fill dimensions (crop to fit)
await sharp('input.jpg')
.resize(800, 600, { fit: 'cover' })
.toFile('output.jpg');
// Width only (auto height)
await sharp('input.jpg')
.resize({ width: 800 })
.toFile('output.jpg');
// Without upscaling
await sharp('input.jpg')
.resize(2000, null, { withoutEnlargement: true })
.toFile('output.jpg');
Fit Options
| Option | Description |
|---|---|
cover |
Crop to cover dimensions (default) |
contain |
Fit within, add background if needed |
fill |
Stretch to fill (ignores aspect ratio) |
inside |
Fit within, never exceed |
outside |
Fit to cover, may exceed |
Position (for cover/contain)
await sharp('input.jpg')
.resize(800, 600, {
fit: 'cover',
position: 'top' // top, right top, right, right bottom, bottom, left bottom, left, left top, center
})
.toFile('output.jpg');
// Or use gravity
await sharp('input.jpg')
.resize(800, 600, {
fit: 'cover',
position: sharp.strategy.attention // Focus on interesting region
})
.toFile('output.jpg');
Format Conversion
// To WebP
await sharp('input.jpg')
.webp({ quality: 80 })
.toFile('output.webp');
// To AVIF (best compression)
await sharp('input.jpg')
.avif({ quality: 60 })
.toFile('output.avif');
// To PNG
await sharp('input.jpg')
.png({ compressionLevel: 9 })
.toFile('output.png');
// To JPEG
await sharp('input.png')
.jpeg({ quality: 80, mozjpeg: true })
.toFile('output.jpg');
// Auto format based on input
await sharp('input.jpg')
.toFormat('webp', { quality: 80 })
.toFile('output.webp');
Format Options
// JPEG
.jpeg({
quality: 80,
progressive: true,
mozjpeg: true, // Better compression
})
// PNG
.png({
compressionLevel: 9,
palette: true, // For fewer colors
quality: 80, // For palette mode
})
// WebP
.webp({
quality: 80,
lossless: false,
nearLossless: false,
effort: 4, // 0-6, higher = slower + smaller
})
// AVIF
.avif({
quality: 60,
effort: 4, // 0-9, higher = slower + smaller
chromaSubsampling: '4:4:4',
})
Operations
Crop/Extract
// Extract region
await sharp('input.jpg')
.extract({ left: 100, top: 100, width: 300, height: 200 })
.toFile('output.jpg');
// Trim whitespace/borders
await sharp('input.jpg')
.trim()
.toFile('output.jpg');
Rotate & Flip
// Rotate (auto from EXIF by default)
await sharp('input.jpg')
.rotate(90) // Degrees clockwise
.toFile('output.jpg');
// Flip
await sharp('input.jpg')
.flip() // Vertical
.flop() // Horizontal
.toFile('output.jpg');
Color Adjustments
await sharp('input.jpg')
.grayscale()
.toFile('output.jpg');
await sharp('input.jpg')
.tint({ r: 255, g: 200, b: 200 })
.toFile('output.jpg');
await sharp('input.jpg')
.modulate({
brightness: 1.2, // 1 = no change
saturation: 0.8,
hue: 180, // Degrees
})
.toFile('output.jpg');
await sharp('input.jpg')
.negate()
.toFile('output.jpg');
Effects
// Blur
await sharp('input.jpg')
.blur(5) // Sigma value, 0.3-1000
.toFile('output.jpg');
// Sharpen
await sharp('input.jpg')
.sharpen() // Default
.toFile('output.jpg');
await sharp('input.jpg')
.sharpen({
sigma: 1,
m1: 1,
m2: 3,
})
.toFile('output.jpg');
// Normalize (stretch contrast)
await sharp('input.jpg')
.normalize()
.toFile('output.jpg');
Composite (Overlays)
// Add watermark
await sharp('input.jpg')
.composite([
{
input: 'watermark.png',
gravity: 'southeast',
blend: 'over',
}
])
.toFile('output.jpg');
// Multiple overlays
await sharp('base.jpg')
.composite([
{ input: 'layer1.png', top: 0, left: 0 },
{ input: 'layer2.png', top: 100, left: 100, blend: 'multiply' },
{
input: Buffer.from('<svg>...</svg>'),
top: 50,
left: 50,
}
])
.toFile('output.jpg');
Add Background/Extend
// Add padding with background color
await sharp('input.png')
.extend({
top: 20,
bottom: 20,
left: 20,
right: 20,
background: { r: 255, g: 255, b: 255, alpha: 1 }
})
.toFile('output.png');
// Flatten transparency
await sharp('input.png')
.flatten({ background: '#ffffff' })
.toFile('output.jpg');
Pipeline Chaining
// All operations chain together
await sharp('input.jpg')
.resize(800, 600, { fit: 'cover' })
.rotate(90)
.sharpen()
.modulate({ brightness: 1.1 })
.webp({ quality: 80 })
.toFile('output.webp');
Metadata
// Get image info
const metadata = await sharp('input.jpg').metadata();
console.log(metadata);
// { width, height, format, space, channels, depth, density, hasAlpha, ... }
// Get stats (pixel analysis)
const stats = await sharp('input.jpg').stats();
console.log(stats);
// { channels: [{ min, max, sum, squaresSum, mean, stdev, ... }] }
Streams & Buffers
import fs from 'fs';
// Stream input/output
const readStream = fs.createReadStream('input.jpg');
const writeStream = fs.createWriteStream('output.webp');
readStream
.pipe(sharp().resize(800).webp())
.pipe(writeStream);
// Buffer to buffer
const inputBuffer = fs.readFileSync('input.jpg');
const outputBuffer = await sharp(inputBuffer)
.resize(400)
.toBuffer();
// With info
const { data, info } = await sharp(inputBuffer)
.resize(400)
.toBuffer({ resolveWithObject: true });
console.log(info);
// { format, width, height, channels, size }
Next.js / API Routes
// app/api/image/route.ts
import { NextRequest, NextResponse } from 'next/server';
import sharp from 'sharp';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const url = searchParams.get('url');
const width = parseInt(searchParams.get('w') || '800');
const quality = parseInt(searchParams.get('q') || '80');
// Fetch original image
const response = await fetch(url!);
const buffer = Buffer.from(await response.arrayBuffer());
// Process
const processed = await sharp(buffer)
.resize(width)
.webp({ quality })
.toBuffer();
return new NextResponse(processed, {
headers: {
'Content-Type': 'image/webp',
'Cache-Control': 'public, max-age=31536000',
},
});
}
Generate Thumbnails
async function generateThumbnails(inputPath, outputDir) {
const sizes = [
{ name: 'thumb', width: 150, height: 150 },
{ name: 'small', width: 400 },
{ name: 'medium', width: 800 },
{ name: 'large', width: 1200 },
];
const image = sharp(inputPath);
const metadata = await image.metadata();
const basename = path.basename(inputPath, path.extname(inputPath));
const results = await Promise.all(
sizes.map(async ({ name, width, height }) => {
const outputPath = path.join(outputDir, `${basename}-${name}.webp`);
await sharp(inputPath)
.resize(width, height, { fit: height ? 'cover' : 'inside' })
.webp({ quality: 80 })
.toFile(outputPath);
return { name, path: outputPath };
})
);
return results;
}
Handle Uploads
import formidable from 'formidable';
import sharp from 'sharp';
async function handleUpload(req) {
const form = formidable();
const [fields, files] = await form.parse(req);
const file = files.image[0];
// Validate and process
const metadata = await sharp(file.filepath).metadata();
if (!['jpeg', 'png', 'webp'].includes(metadata.format)) {
throw new Error('Invalid format');
}
// Process and save
const filename = `${Date.now()}-${file.originalFilename}`;
const processed = await sharp(file.filepath)
.resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 80 })
.toFile(`./uploads/${filename}.webp`);
return {
url: `/uploads/${filename}.webp`,
width: processed.width,
height: processed.height,
};
}
Batch Processing
import sharp from 'sharp';
import { glob } from 'glob';
import path from 'path';
async function batchOptimize(inputGlob, outputDir) {
const files = await glob(inputGlob);
const results = await Promise.all(
files.map(async (file) => {
const basename = path.basename(file, path.extname(file));
const output = path.join(outputDir, `${basename}.webp`);
const info = await sharp(file)
.resize(1920, 1080, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 80 })
.toFile(output);
return { input: file, output, size: info.size };
})
);
return results;
}
Best Practices
- Use streams for large files to reduce memory
- Set concurrency with
sharp.concurrency(1)for low-memory environments - Pre-compute sizes when possible (eager thumbnails)
- Use WebP or AVIF for best compression
- Cache processed images - don't reprocess on every request
- Handle EXIF rotation - Sharp auto-rotates by default
More from mgd34msu/goodvibes-gemini
chakra-ui
Builds accessible React applications with Chakra UI v3 components, tokens, and recipes. Use when creating styled component systems, theming, or accessible form controls.
70fastify
Builds high-performance Node.js APIs with Fastify, TypeScript, schema validation, and plugins. Use when building fast REST APIs, microservices, or needing schema-based validation.
2code-smell-detector
Detects code smells, anti-patterns, and common bugs with quantified thresholds and severity scoring. Use when reviewing code quality, finding maintainability issues, detecting SOLID violations, or identifying technical debt.
2playwright
Tests web applications with Playwright including E2E tests, locators, assertions, and visual testing. Use when writing end-to-end tests, testing across browsers, automating user flows, or debugging test failures.
2vitest
Tests JavaScript and TypeScript applications with Vitest including unit tests, mocking, coverage, and React component testing. Use when writing tests, setting up test infrastructure, mocking dependencies, or measuring code coverage.
2vite
Builds web applications with Vite including dev server, production builds, plugins, and configuration. Use when scaffolding projects, configuring build tools, optimizing bundles, or setting up development environments.
2