The safest place to optimize assets is after your bundler runs and before you deploy. At that point the build has already wired up every file path in your HTML and CSS, so as long as you keep file extensions intact, nothing breaks. This guide recompresses a dist/ folder in CI and deploys the smaller output — on Vercel, Netlify, GitHub Pages, or any static host.
Everything runs against @assetopt/cli (open source, MIT).
The one rule: don’t convert formats on built output
This is the difference between this guide and the WebP / AVIF guides.
Those guides convert .jpg → .webp, which changes the file extension. That’s fine when you’re preparing source assets and then reference the new names yourself. But your built HTML already points at hero.jpg — rename it to hero.webp and you get silent 404s in production.
So on build output, use assetopt’s default outputFormat: "keep": recompress every asset, keep the same name and extension. Same URLs, fewer bytes.
Install
npm install -g @assetopt/cli
In CI you’ll usually call it with npx instead (no global install needed).
A safe .assetoptrc for build output
{
"images": {
"quality": { "jpeg": 82, "png": 80, "webp": 82, "avif": 72 },
"stripMetadata": true
},
"css": { "minify": true },
"js": { "minify": true },
"svg": { "multipass": true },
"output": { "dir": "./optimized" }
}
No preset and no outputFormat — every extension is preserved. (web-perf deliberately converts formats, so it’s the wrong choice here.)
Check it once locally
npm run build
assetopt analyze dist
analyze is a dry run — it reports the savings without writing anything. When the numbers look right, move it into CI.
Wire it into CI
GitHub Actions
- run: npm ci
- run: npm run build
- run: npx assetopt optimize dist --min-savings 5
- run: # your deploy step, pointing at ./optimized/
--min-savings 5 turns assetopt into a quality gate: if total savings drop below 5%, the step exits non-zero and CI fails. Start low (3–5%) and tighten once you have a baseline — a project that already ships optimized assets will fail a high threshold every run.
Persist the cache between runs so unchanged files aren’t re-encoded:
- uses: actions/cache@v4
with:
path: ./optimized
key: assetopt-${{ hashFiles('dist/**') }}
Vercel / Netlify
On a host that builds for you, fold the optimize step into your build command and publish the optimized folder:
- Build command:
npm run build && npx assetopt optimize dist - Output / publish directory:
optimized
The originals in dist/ are never modified; the deployable copy lives in ./optimized/.
What a run looks like
Illustrative output (recompression only — your numbers depend on how optimized your source already is):
image hero.jpg 412.3 KB → 147.8 KB -64.1%
css main.css 48.7 KB → 34.2 KB -29.7%
js app.js 186.5 KB → 112.9 KB -39.4%
svg icon-arrow.svg 1.2 KB → 0.6 KB -50.0%
Saved 1.0 MB (61.0%) · 0 cached · 1.8s
Most of the payload win comes from images; CSS/JS/SVG minification adds a steady few percent on top. You can reproduce real per-file numbers on the downloadable sample pack.
Common pitfalls
- Deploy
./optimized/, not./dist/. Forgetting this makes the whole pipeline a no-op. A guard like[ -d ./optimized ] || exit 1in the deploy script catches it. - Don’t run
optimizeonsrc/orpublic/. Those aren’t processed by your build yet; target the build output. See the CI pre-deploy workflow for the full rationale. - Never set
outputFormat: "webp"on build output — every.jpgbecomes.jpg.webpand your HTML 404s silently.
Next steps
- CI pre-deploy workflow — the reference version with more detail
- CLI reference → optimize — every flag and exit code
- Convert JPG & PNG to WebP in bulk — when you do control the references