Web Performance · Guide
Convert a Website's Images to WebP and AVIF, and Keep Them That Way
34 min read · October 14, 2026 · Mochify Engineering Team
To convert images to WebP and AVIF for a whole website you need two things, and almost every
tool, tutorial and plugin gives you only the first: a way to convert the images you have, and
a way to convert the images that arrive next week without anyone remembering to. This guide
gives you both. The first is one command that runs the same on a Mac, a Windows PC and a Linux
server, with nothing to compile. The second is a twelve-line script that converts only the
files that do not have a .webp and .avif beside them yet, so it can run
every day, in a build, or from an AI coding agent, and cost nothing when there is nothing new.
If you are here because a speed test said "Serve images in modern formats" (Lighthouse and
PageSpeed Insights; older reports said "next-gen formats"), you have met the symptom. If you
are here because the thing that used to fix it stopped working, you have met the real problem.
The usual story goes like this: an npm install that used to work now ends with
"Please install the libjpeg development package before building"; a build step prints "Images
Optimized" and the output folder is empty; the Squoosh command-line tool you installed in 2022
now answers RuntimeError: abort(Error: ENOENT and, it turns out, has carried "Project
no longer maintained" on its npm page since January 2023; or a WordPress plugin with half a million
installs converted everything and now "Images cannot be displayed after conversion", even after
disabling it. Every one of those is a real thread from the last two years. The platform defaults
did not help because there are no defaults: WordPress core stores and serves uploads in the format
they arrived in, static-site hosts serve whatever is in the folder, and a CDN negotiates formats
only if you pay for the feature and switch it on. Images are one of the few parts of page speed
you can take control of yourself, and this particular piece of control is not a chore once the conversion
lives somewhere that does not rot.
This guide is for the person who owns the site, whether or not they write code: a front-end developer, a designer who maintains a portfolio, a small agency, a shop owner on a static builder. It opens with why pipelines like the one above break, so that the one you build next does not. Then it gives you the one-off conversion, the script that keeps it converted, the one line of markup that serves the new files, and one honest paragraph on WordPress. If you have never opened a terminal, start with the six words below and read the worked examples; the commands are short, and each one is explained.
Published October 14, 2026 by the Mochify Engineering Team. Written for the site whose image step rotted: the one-off conversion, the twelve-line script that keeps it converted, and the markup that serves it, with tool status dated October 2026.
Six words this guide uses
Everything below assumes only these terms:
Format: the kind of file an image is saved as. JPEG and PNG are the old universal ones. WebP (2010) and AVIF (2019) store the same picture in a much smaller file, which is why speed tests ask for them. In caniuse's September 2026 data, 97.18% of people browse with something that opens WebP and 96.35% with something that opens AVIF (caniuse.com/webp, caniuse.com/avif).
Convert: make a WebP or AVIF file from a JPEG or PNG. The original stays where it is; the new file sits beside it. Converting is also compressing: a 54 KB
lobby.jpgbecomes alobby.avifof about 22 KB with nothing else changed (the figures are from our own run in the quality section).Lossy and lossless: a lossy encode throws away detail the eye does not notice to make the file small; a lossless encode keeps every pixel exactly and makes a bigger file. Photographs want lossy. Logos, diagrams and screenshots sometimes want lossless.
Fallback: the file a browser gets when it cannot open the new format. For a site that is the JPEG or PNG you already have, which is why you keep it.
The terminal: the text window where you type commands (Terminal on a Mac, PowerShell or Windows Terminal on Windows, any shell on Linux). Three of the examples in this guide are typed there. None is longer than one line, and each is explained.
Pipeline: whatever makes the conversion happen without you doing it by hand: a step in a build, a script that runs on a schedule, a plugin that watches uploads, a CDN that converts on the way out. The pipeline is the second half of the problem, and the half nothing on page one of Google answers.
Why your last image pipeline broke
Most image pipelines break for one of four reasons, and usually two at once: they depended on native code compiled on one particular machine, they depended on a package nobody maintains anymore, only one person understood them, and they converted the library once with no idea what to do about the next upload. Knowing which of those hit you is what tells you what to build instead.
Here is the version we hear most often, told as one site. A studio builds a portfolio site
in 2023: 400 JPEGs in /images, a static generator, and a build step that runs imagemin with the WebP plugin so every JPEG gets a .webp twin. It works. Eighteen months
later a developer gets a new laptop, runs npm install, and the install fails
inside the WebP plugin with a message about a missing libjpeg development package, because
that plugin downloads or compiles a native encoder for the machine it is on. The build is
red. The quickest fix is to comment the step out "for now". The site still deploys, the old .webp files are still there, and nobody notices that the 60 photographs added since
then ship as 2 MB JPEGs, until a client runs PageSpeed Insights before a pitch and the modern-formats
warning is back. By then the person who set the step up has left, the plugin's issue tracker has
an open "Console shows 'Images Optimized', but there are no images in output folder" that nobody
has answered, and the obvious replacement, Squoosh's command-line tool, turns out to have been
abandoned.
That last point deserves its own line, because "squoosh cli" is still what people search
for. The @squoosh/cli package on npm opens with "Project no longer maintained";
its last release, 0.7.3, was published on January 3, 2023 (npmjs.com/package/@squoosh/cli). The web app at squoosh.app is still supported, but it converts one image at a time in a
browser tab, which is no use for 400 of them. imagemin itself is still
published (9.0.1, March 2025), but the format plugins that do the work are separate packages
with their own native dependencies, and that is where the install failures live. sharp, the Node library most build tools use underneath, is well maintained
(0.35.5 shipped on September 27, 2026) and says that "most modern macOS, Windows and Linux
systems do not require any additional install or runtime dependencies" (sharp.pixelplumbing.com). It is also honest about the rest of the sentence: its install page is a list of the ways
a native binary can fail, from Node version floors to lockfiles shared across operating
systems to a C++ toolchain for source builds. None of that is sharp's fault. It is what
native code costs.
The WordPress version of the story is shorter. A converter plugin rewrites every upload and
adds rules to the web server so that a request for photo.jpg quietly receives photo.jpg.webp. It works until the site moves host, the rewrite rules do not
come along, and every converted image is now a broken image. That is the "cannot be
displayed after conversion" thread, and it is why this guide treats "where does the
conversion live" as the first design decision rather than the last.
The common thread is not that these tools were bad. It is that the conversion was welded to one machine, one package or one host, and when any of those changed, the pipeline died silently while the site kept publishing. A pipeline that holds has three properties: it has no native dependency to compile, it can tell the difference between a converted image and an unconverted one, and it is short enough that the next person can read it in a minute. The rest of this guide builds that.
The three places conversion can live, and when you need none of them
Conversion can happen in a build step, at upload time, or on the way out through a CDN; each has one way of failing, and if your framework or CDN already does it for you, you can skip this guide's scripts and only check that the output is right.

A build step. Images are converted when the site is built or deployed. This is the right place for a static site and for most hand-authored HTML, because the converted files end up in the same folder as the originals and get deployed with them. Its failure mode is the one above: the step depends on something installed on the build machine. The fix is a converter that is a plain binary with no compile step, and a script that only converts what is new, so a build never spends ten minutes reconverting 400 files it already did.
An upload hook. Images are converted the moment they enter the CMS. This is how WordPress plugins and most hosted builders do it, and it is the right place when non-technical people add images through an admin screen. Its failure mode is that the hook belongs to the platform: it converts new uploads only, it runs with whatever image library the host installed, and it travels badly between hosts. The WordPress paragraph covers what core does and does not do here.
A CDN that negotiates. The originals stay as JPEG and the CDN converts each
request on the way out, choosing the format from the browser's Accept header,
which is the line every browser sends saying which formats it can open. Cloudflare Polish
does this for WebP only, "served only when the Accept header from the browser
includes WebP", and it is not available on Cloudflare's free plan; Cloudflare Images format=auto and Fastly Image Optimizer auto=avif do it for AVIF as well (Cloudflare Polish docs). This is the right place for a large site with many editors, because nothing about the
repository or the CMS changes. Its failure modes are cost, a free-tier line you may be on
the wrong side of, and the fact that your markup still says .jpg, so the moment
the site leaves that CDN the modern formats leave with it.
When you need none of this. If your site is built with Next.js and uses its <Image> component, the framework already negotiates by Accept header; its default formats is ['image/webp'], and setting it to ['image/avif', 'image/webp'] turns AVIF on, with WebP as the fallback (Next.js image docs). Astro's <Picture /> and Eleventy's image plugin generate the
converted files and the markup for you, with sharp underneath, and let you name AVIF and
WebP as the formats. If you are on one of those, the ongoing problem is solved for every
image the framework handles, and the only thing to check is the serving section. If your whole site has twenty images, you also need
none of this: convert them once in the web app, paste the
markup, and come back when you add the twenty-first.
Everyone else, which is most hand-built sites, most static builders, and every site where "the pipeline" is a person, continues.
Convert the images you have, once: the native toolkit and its five traps
You can convert a folder of images to WebP and AVIF with free command-line encoders in one loop each; the loops people copy from tutorials have five specific bugs, and you should know them even if you then use the one-command route in the next section, because the ongoing script inherits the same shape.
The encoders are cwebp, which "compresses an image using the WebP format" and
is used as cwebp [options] input_file -o output_file.webp (Google's cwebp reference), and avifenc from libavif, used as avifenc -q 75 input.jpg output.avif. ImageMagick reads and writes both formats
once the WebP delegate library is installed. For Node people, sharp("lobby.jpg").avif().toFile("lobby.avif") and .webp() do the same in two lines, with the install caveats above.
Here is the simplest version, for one file, which is where to start if you have never typed
a command. Put lobby.jpg in a folder, open a terminal in that folder, and type:
cwebp -q 80 lobby.jpg -o lobby.webp
avifenc -q 75 lobby.jpg lobby.avifYou now have three files called lobby. The JPEG is unchanged. The -q number is the quality on each tool's own scale; cwebp defaults to 75 if you leave
it out.
Here is the folder version, as it appears in a tutorial that ranks for "bulk convert images to webp", slightly tidied:
for f in *.jpg; do
cwebp -q 80 "$f" -o "${f%.jpg}.webp"
doneRun on the studio's 400 images it looks like it worked, and it has five problems:
It misses
.JPG,.jpegand every subfolder.*.jpgmatches lowercase.jpgin the current folder only. Cameras write.JPG, exports write.jpeg, and the images are in/images/2024/,/images/2025/and so on. The 60 files you most wanted are the ones it skipped.It applies a lossy quality to PNGs the moment someone widens the pattern. Extend the loop to
*.pngand the logo and the diagrams come back as lossy WebP with faint noise around every edge.cwebphas a-losslessswitch; the loops never set it.It overwrites silently. If
lobby.webpalready exists, from an earlier run or from a different source, it is replaced with no message.It redoes everything, every time. There is no check for "does the
.webpalready exist", so a build that runs this spends the same ten minutes on 400 files whether 0 or 60 of them are new. That is the four-minute build people eventually comment out.It has a variable that is never set. The version of this loop in a 2023 blog post that still ranks for the bulk query reads a
pathand then loops over$dir_path, a different name, so it runs over nothing. Copying scripts is fine; reading them first is better.
The fixed version of the same loop uses find, which walks subfolders, with -iname, which ignores case, and skips any image that already has its twin:
find ./images -type f \( -iname '*.jpg' -o -iname '*.jpeg' \) | while read -r src; do
[ -f "${src%.*}.webp" ] || cwebp -q 80 "$src" -o "${src%.*}.webp"
doneRead it as: find every file under images whose name ends in .jpg or .jpeg in any case; for each one, if a file with the same name and .webp does not already exist, make it. That is the whole idea of the ongoing script in the keep-converted section, and it is worth understanding here,
where the only moving part is cwebp.
What this toolkit cannot fix is the install. cwebp and avifenc are two
separate packages that have to be present on every machine that runs the loop, in versions that
agree with each other, and ImageMagick's WebP and AVIF support depends on which delegate libraries
the person who installed it included. On your own laptop that is a one-time job. On a colleague's
laptop, a CI runner, or a server, it is the job that gets skipped and the pipeline that silently
stops. If you want the loop without the install, that is the next section.
Mochify Workflow: convert a whole site in one command, on any machine
The fastest way to convert a site's images to WebP and AVIF is to describe the result rather
than configure it, and to use a converter that is one binary with nothing to compile:
Mochify's Magic Flow in the browser for a handful of files, the mochify command line
for a whole folder tree, the Chrome extension for the one image you are looking at, and the REST
API when the conversion belongs inside a deploy script. All four are clients of the same engine,
so the files they produce are the same.
In the browser, for a few dozen images
Open Magic Flow, drop the JPEGs (3 per batch with a free account, 25 on a paid plan) and type
convert to avif. Download the results. Drop the same files again and typeconvert to webp. If the images are going into a slot 1200 pixels wide, say so,convert to avif, 1200px wide, and they come back resized as well, which on a camera JPEG is usually a bigger saving than the format change. For a logo or diagram with transparency,convert to webp, losslesskeeps every pixel, and you keep the PNG as the fallback; AVIF has no lossless path. The transparency guide covers what survives where.From the terminal, for the whole site
The
mochifycommand is a client over the same API. It installs as one binary (brew install mochifyon a Mac,scoop install mochifyon Windows, a downloaded binary on Linux, orcargo install mochify), signs in once through your browser, and reads a list of file paths fromfind, which is the part that fixes traps 1 and 4 above:convert-site.shbrew install mochify # Windows: add the Scoop bucket, then scoop install mochify mochify auth login # opens your browser once; credentials are saved locally find ./images -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) | mochify -t avif find ./images -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) | mochify -t webpFollow
images/2025/lobby.jpgthrough the third line.findprints its path;mochifyreads the path, sends the file toapi.mochify.app, receives the AVIF, and writes it beside the original asimages/2025/lobby.avif. Because the format changed, no_mochifiedsuffix is added, so the new file has exactly the name the markup in the serving section expects.-tis the output type (jpg,png,webp,aviforjxl);-o ./outsends the results to a separate folder instead if you want them apart;-w 1200resizes on the way;-q 70fixes the quality instead of leaving it on automatic;--losslessis for WebP, PNG and JPEG XL output only; and-p "convert to avif, 1200px wide"is the Magic Flow form if you would rather describe it. The CLI works four files at a time by default (-jchanges that). The complete list is in the CLI README. One thing it does not have is a watch mode or a "skip existing" switch, which is why the script in the next section exists.A 400-image site converted to both formats is 800 output files, and each output file counts as one image against your plan's monthly allowance. Check that number before you start: it is more than a Seller month (300) and fits inside a Pro month (1,200) or a Growth month (5,000); a free account has 25 a month, 3 without an account; and a Day Pass ($2) covers 100 uploads in 24 hours with no account for a site in between.
In Chrome, for one image
Right-click any image on a page, choose Mochify, then "Convert to" and WebP or AVIF. It is a direct conversion with no prompt and no settings, one image at a time, and the file lands in your Downloads folder. The Mochify for Chrome page has the install link. Useful for the one hero image you are fixing today; not a batch tool.
From a deploy script, through the API
The same conversion is one
POSTper file to/v1/squish, with the output type in the query string and the key as a Bearer token, which is the form to use in a build or deploy step that cannot open a browser:deploy-step.shcurl -X POST "https://api.mochify.app/v1/squish?type=avif" \ -H "Content-Type: image/jpeg" \ -H "Authorization: Bearer mchy_your_api_key" \ --data-binary @images/2025/lobby.jpg --output images/2025/lobby.aviftype=webpfor the other twin;quality,width,losslessandstripExifare the other parameters you are likely to want, and the API docs have the same call in JavaScript and Python. The CLI accepts the same key through its-kflag or aMOCHIFY_API_KEYenvironment variable for a machine with no browser to sign in from; on your own computer,mochify auth loginis the normal path.
Privacy, for this workflow. Images travel to api.mochify.app over HTTPS, are streamed into the encoder in memory, and are wiped the moment the encode finishes:
no disk writes, no logs containing file data, never used to train AI. That is the same on the
web app, the CLI, the extension and the API; the CLI writes the result to your disk and nothing
stays on the server. (The hosted MCP server, used by AI agents, is the one surface that keeps
the compressed output for up to five minutes behind a short-lived URL so the agent can fetch it,
and the agent section below says when that matters. Video is Mochify's
one format that is processed in your browser and never leaves your device; images are not, and
we do not claim otherwise.)
Keep them converted: a script that only converts new images
The script that keeps a site converted is the fixed loop from the toolkit section with mochify in place of cwebp: it finds every JPEG and PNG that does not yet have a .avif beside it, sends only those, then does the same for .webp. Run with nothing
new, it sends nothing and costs nothing. That is the whole of "keep them converted", and it
is twelve lines.
Save this as convert-new.sh in the folder above images:
#!/bin/bash
# convert-new.sh: make an AVIF and a WebP beside every JPEG or PNG that does not have them yet
set -e
list=$(mktemp)
for fmt in avif webp; do
find ./images -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) \
| while read -r src; do [ -f "${src%.*}.$fmt" ] || echo "$src"; done > "$list"
if [ -s "$list" ]; then
echo "$(wc -l < "$list") file(s) need a .$fmt"
cat "$list" | mochify -t "$fmt"
else
echo "nothing new for .$fmt"
fi
done
rm -f "$list"Make it runnable once with chmod +x convert-new.sh, then run it with ./convert-new.sh.
Here is a worked run. On Monday the studio adds three photographs: images/2026/atrium.jpg, images/2026/Reception.JPG and images/2026/floor-plan.png.
Everything else already has its twins from the one-off conversion. The script runs and
prints:
3 file(s) need a .avif
3 file(s) need a .webpand the folder now holds atrium.avif, atrium.webp, Reception.avif, Reception.webp, floor-plan.avif and floor-plan.webp, in the same subfolder as their sources, with the same base
names, including the capital R. Six files were sent and six were charged; the other 400
sources were skipped on each pass. Run it again on Tuesday with no new images and it prints nothing new for .avif and nothing new for .webp, sends nothing, and finishes in under a second.
Here is the realistic broken case. Wednesday, a colleague replaces atrium.jpg with a re-edited version, same name. The script sees that atrium.avif exists
and skips it, so the site serves the new JPEG to the 3% of visitors on old browsers and the
old AVIF to everyone else. The script is doing exactly what it was told: "has a twin" is not
"has a current twin". The fix is one of two habits: delete the twins when you replace a
source (rm images/2026/atrium.avif images/2026/atrium.webp, then run the
script), or, if replacements are common, change the test on line 7 to [ "${src%.*}.$fmt" -nt "$src" ], which reads "is the twin newer than
the source", so an edited JPEG is reconverted automatically. The second form is the one to
use in a build.

Where to run it. On your own machine, run it before you deploy, or put it
in the script that deploys. In a build or on a server, it runs the same way; the one
difference is that there is no browser to sign in with, so the runner gets a key from your
dashboard through the MOCHIFY_API_KEY environment variable (the CLI README
documents it as an override; mochify auth login remains the normal path on a
computer with a browser). In a Git hook, it runs before each commit, so a .jpg never
lands in the repository without its twins. In every case the script is the same twelve lines,
there is nothing to compile, and the person who inherits it can read it in a minute, which was
the third property of a pipeline that holds.
From an AI coding agent. If a coding agent maintains the site, the agent
can run convert-new.sh like any other command, or call Mochify directly through
its MCP server: the local server (mochify serve, the same binary, started once
and wired into the agent's config) converts a file and returns the file path and metadata,
so no image bytes ever enter the agent's context; the hosted server at mcp.mochify.app needs no install, is authorized with OAuth, and returns a short-lived download URL on files.mochify.app that expires after about five minutes. Our guide to how the two MCP servers work covers the choice, and the Claude Code guide covers setting the local server up inside Claude Code specifically. If the images are themselves
generated by an AI tool, the AI-generated images guide covers what those inputs need before this script sees them.
When this script is not needed. If images only ever reach the site through a CMS upload screen, the upload hook (a plugin, or your framework's image component) is the right owner and this script would be a second, competing pipeline. Pick one. If the site is a static builder that converts on upload for you, the same applies. The script is for the site whose images arrive as files in a folder.
Serve them: the picture tag, or a CDN that negotiates for you
Converted files do nothing until the page offers them, and there are two ways to do that:
the <picture> element, which lists the AVIF, the WebP and the JPEG so the
browser picks the first one it can open, or a CDN that reads the browser's Accept header and converts on the way out. The markup is the subject of our guide to the HTML picture tag; this section
gives you the one snippet and the check.
For lobby.jpg with its two twins beside it, replace <img src="images/2025/lobby.jpg" ...> with:
<picture>
<source srcset="images/2025/lobby.avif" type="image/avif">
<source srcset="images/2025/lobby.webp" type="image/webp">
<img src="images/2025/lobby.jpg" alt="The studio's lobby at dusk"
width="1600" height="1067" loading="lazy" decoding="async">
</picture>The browser reads top to bottom and downloads one file: the AVIF if it can open AVIF, else
the WebP, else the JPEG. A visitor on a 2026 phone gets the 22 KB AVIF; a visitor on an old
office PC gets the JPEG exactly as before; a search crawler reads the <img src>, which is where Google Search Central tells you to put a fallback URL because "some
browsers and crawlers don't understand these attributes" (Google Images best practices). Everything that was on the old <img> (the alt, the width and height, class, loading) stays on the <img>; the <picture> is only a wrapper. The order matters:
AVIF above WebP, or every modern browser takes the WebP and the AVIF files are never served to
anyone.
Check it, click by click. Open the page in Chrome, Edge or Firefox,
right-click, choose Inspect, open the Network tab, click
the Img filter, and reload. You should see lobby.avif in the
list and no request for lobby.webp or lobby.jpg. If you see the
JPEG in a current browser, the type values or the file names are wrong: compare
them letter for letter with the files on disk, including case. If you see a broken image,
the .avif file is not where the markup says it is; the script writes it beside
the source, so the path is the JPEG's path with the extension changed. To see whether a page
uses <picture> at all, right-click, View Page Source,
and search (Ctrl+F or Cmd+F) for <picture.
The CDN alternative. If you would rather leave the markup saying .jpg, a CDN that negotiates formats converts each request to the best format
the browser lists in its Accept header, as the three places section describes. Polish on Cloudflare covers WebP only and is not on the free plan; Cloudflare Images format=auto and Fastly's auto=avif cover AVIF. It works, it costs money, and nothing
changes in your repository, which is also its weakness: leave the CDN and the modern formats
leave with you. Sites that have the files in the folder and the <picture> in the page are portable.
The audit's current name. Lighthouse calls it "Serve images in modern
formats", and since Lighthouse 13 it lives inside the "Improve image delivery" insight; it
names AVIF and WebP and says "You'll need to serve a fallback PNG or JPEG image for older
browser support" (Chrome for Developers). The snippet above is that fallback. Images in CSS backgrounds are a separate case,
because <picture> cannot reach them and Google does not index CSS images at
all; the picture-tag guide has the one-paragraph answer.
WordPress, in one honest paragraph
WordPress core does not convert your JPEG and PNG uploads to WebP or AVIF by default; the
way to get it done without betting the site on a third-party plugin is the WordPress
project's own Modern Image Formats plugin, which converts new uploads only. The details,
briefly: WordPress 6.5 added AVIF support for uploads in February 2024, with the note that
"AVIF in WordPress depends on support in your web server's image processing library" (make.wordpress.org); converting uploads to WebP by default was merged for 6.1 and reverted in September 2022,
and has not returned. The canonical plugin, Modern Image Formats (webp-uploads,
2.7.1, tested up to 7.1, 100,000+ installs), generates AVIF "if supported on the hosting
server, otherwise WebP", keeps the original, and converts "only for new uploads"; existing
images are converted only if you regenerate them, and its <picture> output is a setting in the plugin, not something core emits. Third-party converter plugins add
bulk conversion of the existing library, and some serve the converted files through web-server
rewrite rules; that rewrite layer is the part that moves separately from the site, so check it
after any host change. Our guide to fixing the modern-formats warning in WordPress without plugins walks the core and plugin routes, and the Quick Answer on whether WordPress 7.1 optimizes images without a plugin has the short version. Everything else in this guide applies to a WordPress site's theme images,
which are files in a folder like any other site's.
Quality, size and metadata: what the defaults do
With default settings, converting a JPEG to WebP or AVIF roughly halves the file at the same visual quality. In our own run on October 14, 2026, through the API with the quality left on automatic and nothing resized, a 512 by 512 photograph saved as a JPEG at quality 85 (53,962 bytes) came back as a 22,398-byte WebP and a 22,267-byte AVIF, 58% and 59% smaller; a 600 by 400 photograph (56,809 bytes) came back as a 25,988-byte WebP and a 26,915-byte AVIF, 54% and 53% smaller; and a 1440 by 900 PNG screenshot of a user interface (70,348 bytes) came back as a 45,430-byte WebP and a 29,956-byte AVIF, 35% and 57% smaller. At these sizes the two formats land within a few kilobytes of each other on photographs, and the screenshot is where AVIF pulls ahead; resizing a 4000-pixel camera JPEG to the 1600 pixels the page actually shows saves more than either format change, which is why "convert to avif, 1600px wide" is the prompt we would type first.
Photograph 512 × 512
Photograph 600 × 400
UI screenshot 1440 × 900
Our run, October 14, 2026: through the API, quality on automatic, nothing resized.
Show as a table
| Image | Source | WebP | AVIF |
|---|---|---|---|
| Photograph, 512 × 512 | 53,962 (JPEG q85) | 22,398 | 22,267 |
| Photograph, 600 × 400 | 56,809 (JPEG) | 25,988 | 26,915 |
| UI screenshot, 1440 × 900 | 70,348 (PNG) | 45,430 | 29,956 |
A few defaults are worth knowing before you convert a whole site, because they are the ones people search for afterwards:
The default encode is lossy, at high quality. "Without losing quality" means without losing quality you can see; a lossy WebP or AVIF is not the same bytes as the JPEG, and a JPEG was already lossy. If you have a pixel-exact requirement (a screenshot, a logo, a diagram with thin lines), ask for lossless and check the size:
--losslesson the CLI andlossless=1on the API apply to WebP, PNG and JPEG XL output, AVIF has no lossless path, and the CLI's own README notes that a lossless output "is usually larger than the input: lossless preserves pixels, not file size". Our guide to lossless image formats is the decision in full.Transparency survives. A PNG with a transparent background comes back as a WebP or AVIF with the transparency kept; it is JPEG that flattens to white. Keep the PNG as the fallback in the
<picture>for those images, not a JPEG.Metadata is stripped by default. The CLI strips EXIF unless you pass
--keep-metadata; the API strips it unless you sendstripExif=false. For a website that is what you want: the camera's GPS position should not ship with the lobby photograph. Our guide on EXIF data and the risks of leaving it in covers the cases where you would keep it.A WebP source makes a lossy AVIF twice over. "webp to avif" is a common search, and it works, but the WebP was already a lossy encode, so the AVIF is a re-encode of a re-encode. If the original JPEG or PNG still exists, convert from that.
SVG goes in, WebP comes out. The classic command-line complaint is that the WebP encoder "can't decode .svg files". Mochify accepts SVG on every surface and rasterizes it at the size you ask for, so a logo in SVG can have a WebP and an AVIF twin like everything else; the SVG to WebP converter is the drop-a-file version.
--optimize-for-webon the CLI (optimizeForWeb=1on the API) asks for progressive encoding and 4:2:0 chroma subsampling, which the README describes as the "smallest file to serve". For photographs on a web page it is a reasonable default to add to the script's twomochifylines; for a screenshot with colored text, leave it off and compare.JPEG XL is the newest format and
-t jxlmakes it, but Chrome only began opening it by default with version 155 on October 6, 2026, and caniuse's September figure is 16.95%. Our guide to JPEG XL's arrival in Chrome says when a third twin is worth making; for now, two is the answer.
If what you want is the decision between WebP and AVIF rather than both, the Quick Answer on WebP, AVIF or JPEG XL in 2026 is the short version; this guide's position is that making both costs one extra line and lets the browser decide.
Cheat Sheet
| Task | Answer |
|---|---|
| Convert a folder tree, once | find ./images -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) |
mochify -t avif, then the same with -t webp |
| Convert a few dozen in the browser | mochify.app/flow: drop, type convert to avif, repeat with convert to webp |
| Convert one image on a page | Chrome: right-click, Mochify, "Convert to", pick the format |
| Keep them converted | convert-new.sh above: find sources with no .avif / .webp twin, pipe only those into mochify |
| Reconvert an edited source | Delete its twins and rerun, or test -nt (twin newer than source)
instead of -f |
Match .JPG and subfolders | find with -iname, never *.jpg |
| Resize on the way | -w 1200 (CLI), width=1200 (API), "1200px wide" (Magic Flow) |
| Lossless | --lossless / lossless=1: WebP, PNG, JPEG XL only; AVIF
has none; output is often larger |
| Keep EXIF | --keep-metadata (CLI), stripExif=false (API); stripped by default |
| Serve the files | <picture> with AVIF, then WebP <source>s and
the JPEG or PNG <img>; or a CDN with format=auto / auto=avif |
| Check what loaded | Inspect, Network tab, Img filter, reload: one request, .avif in a current
browser |
| Build or CI without a browser | MOCHIFY_API_KEY environment variable, or the API with Authorization: Bearer |
| WordPress | Core does not convert uploads; Modern Image Formats plugin converts new uploads, AVIF if the server can |
| Support (caniuse, Sep 2026) | WebP 97.18%, AVIF 96.35%, JPEG XL 16.95%; keep the JPEG or PNG fallback |
| Allowance | One per output file: 400 images in two formats is 800; 25 a month free with an account, 3 without; Day Pass $2 for 100 in 24 hours |
FAQ
What about images I upload after converting everything?
They arrive as JPEG or PNG and undo the fix unless something converts them, so the conversion has to live somewhere that runs again: a script that converts only unconverted files (the convert-new.sh in this guide), an upload hook in your CMS, or a CDN that converts on the way out. Run the script before each deploy and every new image gets its WebP and AVIF twins; run it with nothing new and it sends nothing.
Can I convert only the new images instead of the whole folder every time?
Yes. Test for the output file before converting: [ -f "${src%.*}.avif" ] || echo "$src" lists only the sources with no AVIF beside them, and piping that list into mochify -t avif converts just those. For sources that get edited in place, test -nt (newer than) instead, so a changed JPEG is reconverted and an unchanged one is skipped.
Why did my imagemin or Squoosh CLI step stop working?
Because both depend on native code or on a package that is no longer maintained. The @squoosh/cli package carries "Project no longer maintained" on npm and was last published on January 3, 2023; imagemin's format plugins compile or download native encoders for each machine, which is where "install the libjpeg development package" errors come from. A pipeline that holds uses a converter with no compile step and a script short enough to read.
Do I still need to keep the JPEG or PNG?
Yes. In caniuse's September 2026 data about 3 in 100 visitors use a browser that opens neither WebP nor AVIF, and Google asks for a fallback <img src> because some crawlers do not understand <picture>. The JPEG is the file you already had; keeping it beside its twins costs nothing. For images with transparency, the fallback is the PNG.
Does this work on Windows?
Yes. The mochify command installs on Windows through Scoop (add the mochify bucket from the README, then scoop install mochify) and the single-file commands are the same. The find loop and the convert-new.sh script are Bash, so on Windows run them in WSL or Git Bash; or skip the script and use the web app at mochify.app/flow, which needs nothing installed.
Is there a WordPress way to do this without a plugin?
Not for uploads: WordPress core stores and serves uploads in the format they arrived in, and the "WebP by default" change was reverted before 6.1 shipped. The WordPress project's own Modern Image Formats plugin converts new uploads to AVIF (or WebP where the server cannot make AVIF). Theme and template images are files in a folder, and the script in this guide handles those the same way as on any other site.
Can it convert SVG files to WebP and AVIF?
Yes. Mochify accepts SVG as input on every surface and draws it at the size you ask for before encoding, so an SVG logo gets a WebP and an AVIF twin like any other image, transparency included. The command-line WebP encoder cannot read SVG, which is the usual reason the SVGs get left out of a batch.
What is the Lighthouse "next-gen formats" audit called now?
"Serve images in modern formats". Since Lighthouse 13 it sits inside the "Improve image delivery" insight rather than as a separate line, it names AVIF and WebP, and it tells you to keep a PNG or JPEG fallback for older browsers. Older tutorials and some tools still say "next-gen formats"; it is the same check.
Convert your first folder now at mochify.app/flow, or install the CLI from the repository. The prompt to start with is "convert to avif, 1200px wide", then "convert to webp".