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.jpg becomes a lobby.avif of 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.

Diagram of the path from a JPEG source file to the browser, with three places conversion can happen. A build step converts when the site is built, and fails when the install on the build machine breaks and the step gets commented out. An upload hook converts at upload time, and belongs to the platform, so it travels badly between hosts. A CDN converts on every request at the edge, and costs money, while the markup still says .jpg, so leaving the CDN loses the formats. Next.js, Astro and Eleventy already do it for the files they handle.

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:

convert-one.sh
cwebp -q 80 lobby.jpg -o lobby.webp
avifenc -q 75 lobby.jpg lobby.avif

You 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:

tutorial-loop.sh
for f in *.jpg; do
  cwebp -q 80 "$f" -o "${f%.jpg}.webp"
done

Run on the studio's 400 images it looks like it worked, and it has five problems:

  1. It misses .JPG, .jpeg and every subfolder. *.jpg matches lowercase .jpg in 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.

  2. It applies a lossy quality to PNGs the moment someone widens the pattern. Extend the loop to *.png and the logo and the diagrams come back as lossy WebP with faint noise around every edge. cwebp has a -lossless switch; the loops never set it.

  3. It overwrites silently. If lobby.webp already exists, from an earlier run or from a different source, it is replaced with no message.

  4. It redoes everything, every time. There is no check for "does the .webp already 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.

  5. 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 path and 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:

convert-webp.sh
find ./images -type f \( -iname '*.jpg' -o -iname '*.jpeg' \) | while read -r src; do
  [ -f "${src%.*}.webp" ] || cwebp -q 80 "$src" -o "${src%.*}.webp"
done

Read 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.

  1. 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 type convert 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, lossless keeps every pixel, and you keep the PNG as the fallback; AVIF has no lossless path. The transparency guide covers what survives where.

  2. From the terminal, for the whole site

    The mochify command is a client over the same API. It installs as one binary (brew install mochify on a Mac, scoop install mochify on Windows, a downloaded binary on Linux, or cargo install mochify), signs in once through your browser, and reads a list of file paths from find, which is the part that fixes traps 1 and 4 above:

    convert-site.sh
    brew 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 webp

    Follow images/2025/lobby.jpg through the third line. find prints its path; mochify reads the path, sends the file to api.mochify.app, receives the AVIF, and writes it beside the original as images/2025/lobby.avif. Because the format changed, no _mochified suffix is added, so the new file has exactly the name the markup in the serving section expects. -t is the output type (jpg, png, webp, avif or jxl); -o ./out sends the results to a separate folder instead if you want them apart; -w 1200 resizes on the way; -q 70 fixes the quality instead of leaving it on automatic; --lossless is 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 (-j changes 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.

  3. 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.

  4. From a deploy script, through the API

    The same conversion is one POST per 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.sh
    curl -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.avif

    type=webp for the other twin; quality, width, lossless and stripExif are 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 -k flag or a MOCHIFY_API_KEY environment variable for a machine with no browser to sign in from; on your own computer, mochify auth login is 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:

convert-new.sh
#!/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:

output.txt
3 file(s) need a .avif
3 file(s) need a .webp

and 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.

Diagram of convert-new.sh on Monday: in a folder tree, the 400 existing sources already have their .avif and .webp twins and are skipped, while atrium.jpg, Reception.JPG and floor-plan.png in images/2026 have no .avif and are sent, so 6 files are sent and charged across both passes. On Wednesday atrium.jpg is re-edited with the same name: the -f test sees the old atrium.avif and skips it, while the -nt test sees the twin is older than the source and reconverts it. On Tuesday, with nothing new, nothing is sent.

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:

lobby.html
<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.

File size after conversion, in bytes

Photograph 512 × 512

JPEG q85
WebP
AVIF

Photograph 600 × 400

JPEG
WebP
AVIF

UI screenshot 1440 × 900

PNG
WebP
AVIF

Our run, October 14, 2026: through the API, quality on automatic, nothing resized.

Show as a table
ImageSourceWebPAVIF
Photograph, 512 × 51253,962 (JPEG q85)22,39822,267
Photograph, 600 × 40056,809 (JPEG)25,98826,915
UI screenshot, 1440 × 90070,348 (PNG)45,43029,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: --lossless on the CLI and lossless=1 on 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 send stripExif=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-web on the CLI (optimizeForWeb=1 on 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 two mochify lines; for a screenshot with colored text, leave it off and compare.

  • JPEG XL is the newest format and -t jxl makes 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

TaskAnswer
Convert a folder tree, oncefind ./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 browsermochify.app/flow: drop, type convert to avif, repeat with convert to webp
Convert one image on a pageChrome: right-click, Mochify, "Convert to", pick the format
Keep them convertedconvert-new.sh above: find sources with no .avif / .webp twin, pipe only those into mochify
Reconvert an edited sourceDelete its twins and rerun, or test -nt (twin newer than source) instead of -f
Match .JPG and subfoldersfind 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 loadedInspect, Network tab, Img filter, reload: one request, .avif in a current browser
Build or CI without a browserMOCHIFY_API_KEY environment variable, or the API with Authorization: Bearer
WordPressCore 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
AllowanceOne 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".