Four years of optimizing React and Next.js projects taught me one thing: unoptimized images are everywhere, and nobody wants to fix them.
Every project has the same pattern. Heavy PNG and JPG files are sitting inside /public, there is no consistent image pipeline, and some of those files have no business being that large in a production codebase.
This is especially common in small and mid-sized projects. There is no CDN transformation layer or dedicated asset pipeline. Images get added while the product is moving quickly, and the cleanup becomes a task for “later.”
Later, of course, never comes.
Then, at 1am, while refactoring an extremely vibe-coded Next.js project, I found myself doing the cleanup manually again.
Find an image. Upload it to an online compressor. Hit the free limit. Open another tool. Convert a few more. Download everything. Replace the original files. Hunt through the codebase for every import and src path. Hope I did not miss one.
And I finally thought: I am a developer. Why am I doing this by hand?
So I built explains these runtime optimizations in detail.
But that solves a different layer of the problem.
I wanted to clean up the source assets themselves:
- Replace heavy PNG and JPG files with smaller WebP files when conversion is worthwhile.
- Update existing imports and string-based image paths across the repository.
- Identify images that are no longer referenced.
- Make the migration reviewable before touching any files.
- Keep the process useful for projects that do not consistently use
next/image.
This was not about replacing the Next.js image pipeline. It was about making a tedious codebase migration safe and repeatable.
Why a simple format conversion becomes a code migration
Consider a small project with images referenced in a few different ways:
import hero from './assets/hero.png';
export function HomePage() {
return (
<main>
<Image src={hero} alt="" />
<img src="/images/team.jpg" alt="Our team" />
</main>
);
}
Converting hero.png and team.jpg is only half of the job. The code now points to files that no longer represent the desired output.
After the migration, the references need to become:
import hero from './assets/hero.webp';
export function HomePage() {
return (
<main>
<Image src={hero} alt="" />
<img src="/images/team.webp" alt="Our team" />
</main>
);
}
Now multiply that across .js, .jsx, .ts, .tsx, .html, and .json files, plus nested apps inside a Turborepo.
That is no longer an image-conversion task. It is a codemod with image processing attached.
The workflow I wanted
I wanted the tool to perform five clear phases:
- Scan the repository for supported images and source files.
- Determine which images are actually referenced.
- Convert used PNG and JPG files to WebP.
- Update only the references connected to successful conversions.
- Report what changed, what was skipped, and what needs human review.
The default command is intentionally small:
npx pixcrush .
For a first pass on an unfamiliar project, I prefer starting with a dry run:
npx pixcrush . --dry-run
This previews the migration without writing files.
Step 1: Find the images and their references
pixcrush scans for PNG, JPG, and JPEG files while ignoring directories such as node_modules, .next, dist, build, and .git.
It separately collects the source files that may contain references:
.js .jsx .ts .tsx .html .htm .json
Finding image files is easy. Determining whether they are used is the interesting part.
For JavaScript and TypeScript, pixcrush parses the source with Babel using the JSX and TypeScript plugins. Babel’s parser produces an abstract syntax tree, which makes it possible to inspect actual string literals instead of blindly replacing every .png substring. Babel documents its JSX and TypeScript parsing support in the , which supports WebP output and exposes a configurable quality setting.
The important part is not just calling .webp(). pixcrush creates the WebP data in memory first:
const webpBuffer = await sharp(imagePath)
.webp({ quality })
.toBuffer();
Sharp’s
If you try it on a real React, Next.js, or Turborepo project, I would especially love feedback on path-resolution edge cases and file types worth supporting next.
SOCIAL SHARE CARD GENERATOR