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 sharp

Sharp 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.webp

A 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: sharp does not create directories. Call mkdir -p public/images before the script or use node:fs/promises mkdir with { recursive: true }.
  • Images are blurry after resize: the default resize uses lanczos3 resampling, 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.