How to Build an Automated Image Optimization Workflow for Developers (Step-by-Step Guide)
Introduction: Manual Image Optimization Doesn't Scale
Manual image compression is a trap. You do it once, feel productive, then two weeks later a new batch of Figma exports lands in your repository — uncompressed, mis-formatted, and 4x larger than they need to be.
The only reliable solution is automation: an image optimization pipeline that runs without human intervention and guarantees that every image that ships to production is already optimized.
This step-by-step guide shows you how to build exactly that — using free image optimization tools for developers like Sharp, imagemin, npm scripts, and GitHub Actions. No paid services, no black-box SaaS, no vendor lock-in.
Why You Must Automate Your Image Optimization
If you're still compressing images manually before each commit, here's what that workflow actually looks like over time: inconsistent output quality, forgotten files, no clear standards for new team members, and performance regressions that creep in on every sprint.
Automated pipelines solve all of this because they are:
- ✅ Consistent — Every image is processed with identical settings, every time
- ✅ Invisible — Runs as part of your existing workflow with zero additional effort
- ✅ Scalable — Handles 1 image or 1,000 images with the same script
- ✅ Enforceable — Pre-commit hooks and CI checks ensure no unoptimized image can bypass the pipeline
- ✅ Documented — Your quality settings live in version-controlled config, not in someone's memory
Not sure if your images are actually causing a problem? First read: Why Unoptimized Images Are Killing Your Developer Projects.
Step-by-Step: Building Your Automated Image Optimization Pipeline
Install Sharp as a Dev Dependency
Sharp is the fastest Node.js image processing library and the backbone of our pipeline. Install it as a development dependency:
npm install sharp --save-dev
Note: Sharp uses native binaries. If you encounter installation issues on Alpine Linux or ARM-based environments, follow the Sharp installation guide for platform-specific instructions.
Write Your Image Optimization Script
Create a file at scripts/optimize-images.js in your project root:
// scripts/optimize-images.js const sharp = require('sharp'); const fs = require('fs'); const path = require('path'); // Configuration — adjust to your project const CONFIG = { inputDir: './src/assets/images/original', outputDir: './public/images', quality: { webp: 82, jpeg: 85, avif: 65, png: 9 // PNG compression level 0-9 }, maxWidth: 1920 // Resize images wider than this }; // Create output directory if it doesn't exist if (!fs.existsSync(CONFIG.outputDir)) { fs.mkdirSync(CONFIG.outputDir, { recursive: true }); } // Get all image files from input directory const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif']; const files = fs.readdirSync(CONFIG.inputDir) .filter(f => imageExtensions.includes(path.extname(f).toLowerCase())); let processed = 0; let totalSaved = 0; Promise.all(files.map(async (file) => { const inputPath = path.join(CONFIG.inputDir, file); const baseName = path.parse(file).name; const outputPath = path.join(CONFIG.outputDir, `${baseName}.webp`); const originalSize = fs.statSync(inputPath).size; await sharp(inputPath) .resize({ width: CONFIG.maxWidth, withoutEnlargement: true }) .webp({ quality: CONFIG.quality.webp }) .toFile(outputPath); const optimizedSize = fs.statSync(outputPath).size; const saved = Math.round((1 - optimizedSize / originalSize) * 100); totalSaved += (originalSize - optimizedSize); processed++; console.log(`✅ ${file} → ${baseName}.webp (${saved}% smaller)`); })).then(() => { const savedKB = Math.round(totalSaved / 1024); console.log(`\n🎉 Done! Processed ${processed} images. Total saved: ${savedKB}KB`); }).catch(err => { console.error('❌ Optimization failed:', err); process.exit(1); });
Add an npm Script to package.json
Wire your script into package.json so it can be run manually and as part of your build:
{
"scripts": {
"optimize:images": "node scripts/optimize-images.js",
"prebuild": "npm run optimize:images",
"build": "your-existing-build-command"
}
}
Using prebuild ensures images are always optimized before your production build runs —
no separate step to remember.
Set Up a Pre-Commit Hook with Husky
Pre-commit hooks run automatically before each git commit, ensuring no unoptimized image
ever enters your repository. Install husky and lint-staged:
npm install husky lint-staged --save-dev npx husky init
Configure lint-staged in package.json:
{
"lint-staged": {
"src/assets/images/original/*.{jpg,jpeg,png,gif}": [
"node scripts/optimize-images.js",
"git add public/images"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
Now, any time a developer adds an image to the source directory and commits, the optimization script runs automatically before the commit is recorded.
Add a GitHub Actions Workflow for CI/CD
As a final safety net, add a GitHub Actions workflow that runs your optimization script on every push
and pull request. Create .github/workflows/optimize-images.yml:
# .github/workflows/optimize-images.yml
name: Optimize Images
on:
push:
paths:
- 'src/assets/images/original/**'
pull_request:
paths:
- 'src/assets/images/original/**'
jobs:
optimize:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run image optimization
run: npm run optimize:images
- name: Commit optimized images
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'chore: auto-optimize images [skip ci]'
file_pattern: 'public/images/**'
This workflow triggers only when image files in your source directory change, keeping CI run times fast and focused.
Advanced Tips for Your Image Optimization Pipeline
- 🚀 Generate multiple sizes for responsive images — Extend your script to output 400px, 800px, and 1200px variants, then use
srcsetin your HTML - 🚀 Add AVIF as a secondary output — Generate both
.webpand.avifversions, then serve AVIF to supporting browsers via<picture> - 🚀 Cache processed images in CI — Use GitHub Actions cache to skip re-processing images that haven't changed between runs
- 🚀 Add a size budget check — Fail the CI job if any single image exceeds a defined KB threshold (e.g., 300KB for content images)
- 🚀 Log a compression report — Output a summary of bytes saved per image to your CI logs for easy monitoring over time
- 🚀 Integrate with your CDN — After optimization, consider pairing with a CDN that applies on-the-fly format negotiation for older browser fallbacks
Promise.all() (as shown in the script above). This can reduce processing time by 5–10x
compared to sequential processing on large image sets.
Conclusion
You now have a complete, production-ready automated image optimization workflow — from pre-commit hooks that catch unoptimized files before they're committed, to a CI/CD pipeline that processes images automatically on every push.
The investment is roughly 30 minutes of setup time. The return is permanent — every image that ships to production from this point forward will be compressed, converted to WebP, and properly sized. No exceptions.
For the full context on why this matters and which tools power this workflow, return to: Best Free Image Optimization Tools for Developers.
Need help choosing between Sharp, imagemin, or TinyPNG for your pipeline? Read: Squoosh vs TinyPNG vs Sharp — Complete Developer Comparison.
⚡ Need a Quick Win Right Now?
Before your pipeline is set up, compress your most important images instantly with ConvertIImage — free, no install, no account.
Compress Images Now — Free →← Tools Comparison | ← Image Problems Guide | ← Full Developer Guide
FAQs
Node.js v18 or higher is recommended. Sharp v0.33+ (current as of 2026) requires Node.js v18.17.0+.
Use node -v to check your version and nodejs.org to upgrade if needed.
Yes. For Vite, use the vite-imagetools plugin. For Webpack, use image-minimizer-webpack-plugin
with Sharp as the implementation. These integrate directly into your build config and process images at build time
without a separate script.
Best practice is to commit only original images to your source directory (e.g., src/assets/images/original/)
and add the output directory (e.g., public/images/) to .gitignore.
Let your CI pipeline regenerate optimized images on each build. This keeps your repository lean
and avoids storing binary duplicates in Git history.
For user-uploaded images, run Sharp on the server side at the point of upload — not in the build pipeline. Create an upload handler that pipes the incoming file through Sharp for compression and WebP conversion before saving it to storage. Alternatively, use a CDN with on-the-fly transformation like Cloudinary or imgix.