Overview
Sharp is the fastest Node.js image processing library, backed by libvips. Use it in build scripts to resize images to multiple breakpoints, convert to WebP or AVIF, and strip unnecessary metadata before deploying. This replaces manual image optimization and ensures every image that ships is the right size and format. See optimize-images-for-web for format selection strategy.
Prerequisites
- Node 22 and npm or pnpm.
- Source images in the project, typically in
src/images/or a content directory. - A build step where the image script can run (npm script, Vite plugin, or CI step).
Steps
1. Install Sharp
npm install sharp
# or
pnpm add sharpSharp installs prebuilt binaries for the host platform. On CI, the binary is downloaded for the runner’s architecture automatically.
2. Write a basic resize and convert script
Create scripts/optimize-images.mjs:
import sharp from "sharp";
import { readdir } from "node:fs/promises";
import { join, extname, basename } from "node:path";
const INPUT_DIR = "src/images";
const OUTPUT_DIR = "public/images";
const WIDTHS = [400, 800, 1200];
const files = await readdir(INPUT_DIR);
const images = files.filter((f) => /\.(jpg|jpeg|png)$/i.test(f));
for (const file of images) {
const input = join(INPUT_DIR, file);
const name = basename(file, extname(file));
for (const width of WIDTHS) {
// WebP
await sharp(input)
.resize(width)
.webp({ quality: 80 })
.toFile(join(OUTPUT_DIR, `${name}-${width}.webp`));
// AVIF (slower to encode; use for production only)
await sharp(input)
.resize(width)
.avif({ quality: 60 })
.toFile(join(OUTPUT_DIR, `${name}-${width}.avif`));
}
}
console.log(`Optimized ${images.length} images at ${WIDTHS.join(", ")}px.`);3. Add to package.json scripts
{
"scripts": {
"optimize:images": "node scripts/optimize-images.mjs",
"build": "npm run optimize:images && vite build"
}
}Run npm run optimize:images locally before deploying. Add it as a pre-build step in CI.
4. Use <picture> with srcset in HTML
The optimized images are only useful if the HTML serves the right one.
<picture>
<source
srcset="/images/hero-400.avif 400w, /images/hero-800.avif 800w, /images/hero-1200.avif 1200w"
type="image/avif"
/>
<source
srcset="/images/hero-400.webp 400w, /images/hero-800.webp 800w, /images/hero-1200.webp 1200w"
type="image/webp"
/>
<img src="/images/hero-800.jpg" alt="Hero image" width="800" height="450" loading="lazy" />
</picture>AVIF is listed first; browsers pick the first format they support.
5. Cache optimized images in CI
Reprocessing images on every CI run is expensive. Cache the public/images output directory keyed on the source image hashes.
- uses: actions/cache@v4
with:
path: public/images
key: ${{ runner.os }}-images-${{ hashFiles('src/images/**') }}See set-up-github-actions-cache for the full caching pattern.
6. Add Sharp to a Vite pipeline via plugin
For Vite projects, use vite-imagetools or write a custom Vite plugin that calls Sharp during the build. This integrates image optimization into the module graph rather than a separate script.
// vite.config.ts
import { imagetools } from "vite-imagetools";
export default defineConfig({
plugins: [imagetools()],
});
// In a component:
// import hero from "./hero.jpg?w=800&format=webp";Verify it worked
node scripts/optimize-images.mjs
ls -lh public/images/
# Check that WebP and AVIF files exist and are smaller than the originals
# Compare file sizes
du -sh src/images/hero.jpg public/images/hero-800.webpA typical JPEG compressed to WebP at 800px width saves 40 to 70 percent of file size.
Common errors
- Sharp install fails on M1/M2 Mac: run
npm install --platform=darwin --arch=arm64 sharp. For CI, the binary downloads automatically for the runner architecture. - AVIF encoding is very slow: AVIF encoding is CPU-intensive. For large image sets, encode AVIF only in CI and use WebP in local development. Or limit AVIF to the widest breakpoint.
- Output directory not found:
sharpdoes not create directories. Callmkdir -p public/imagesbefore the script or usenode:fs/promisesmkdirwith{ recursive: true }. - Images are blurry after resize: the default
resizeuseslanczos3resampling, which is high quality. If images look soft, check that the source resolution is higher than the target width. - Metadata (EXIF) retained: Sharp strips metadata by default when converting. For JPEG to JPEG operations, add
.withMetadata(false)explicitly to ensure stripping.