I stopped relying on third party image services a while ago. They work, but they add cost, latency, and another dependency I do not control. Running everything directly on the server gives me full control over how images are processed and served, and it removes a lot of unnecessary moving parts.
This setup uses Apache, a simple shell script, and a couple of command line tools to convert images into modern formats like AVIF and WebP. It runs quietly in the background using cron, keeps your image library optimized, and only keeps files that are actually smaller than the originals. The result is faster page loads and less bandwidth usage without touching your existing workflow.
What Next Gen Images Actually Are
When I say next gen images, I am talking about newer image formats like WebP and AVIF. These formats are designed to replace older ones like JPEG and PNG by offering better compression while maintaining quality.
WebP has been around for a while and is supported by most browsers. AVIF is newer and can compress images even further, often producing files that are 30 to 50 percent smaller than JPEG. That reduction matters when you have hundreds or thousands of images being loaded across your site.
The benefit is simple. Smaller images load faster, reduce bandwidth, and improve page performance. Search engines also take this into account, so it can help with SEO without doing anything fancy.
Required Tools for Image Conversion
Before anything works, the server needs the right tools installed. The script depends on two binaries:
avifencfor AVIF conversioncwebpfor WebP conversion
AlmaLinux / CentOS / RHEL
sudo dnf install libavif-tools libwebp-tools -y
Ubuntu / Debian
sudo apt update sudo apt install libavif-bin webp -y
Verify Installation
which avifenc which cwebp
If both commands return a path, you are good to go. If not, the script will fail early and tell you what is missing.
Directory Structure Setup
I keep things simple and predictable. The original images stay where they are, and converted images go into separate directories.
/home/user/public_html/wp-content/uploads /home/user/public_html/wp-content/avif-uploads /home/user/public_html/wp-content/webp-uploads
This separation keeps things clean and avoids overwriting originals. It also makes it easy to remove generated files if needed without touching your source images.
The Conversion Script
This is the core of the system. It scans your upload directory, converts images, and only keeps them if they are smaller than the original. Below is the script (convert-to-avif-webp.sh) adapted for your environment.
#!/bin/bash
SRC_DIR="/home/user/public_html/wp-content/uploads"
AVIF_DIR="/home/user/public_html/wp-content/avif-uploads"
WEBP_DIR="/home/user/public_html/wp-content/webp-uploads"
AVIFENC="$(command -v avifenc)"
CWEBP="$(command -v cwebp)"
if [[ -z "$AVIFENC" ]]; then
echo "ERROR: avifenc not found"
exit 1
fi
if [[ -z "$CWEBP" ]]; then
echo "ERROR: cwebp not found"
exit 1
fi
find "$SRC_DIR" -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) | while read -r src_img; do
rel_path="${src_img#$SRC_DIR/}"
rel_base="${rel_path%.*}"
avif_dest="$AVIF_DIR/${rel_base}.avif"
if [[ ! -f "$avif_dest" ]]; then
mkdir -p "$(dirname "$avif_dest")"
echo "Converting to AVIF: $rel_path"
if ! "$AVIFENC" "$src_img" "$avif_dest"; then
echo " AVIF conversion failed: $rel_path"
rm -f "$avif_dest"
elif [[ -f "$avif_dest" ]]; then
orig_size=$(stat -c %s "$src_img")
avif_size=$(stat -c %s "$avif_dest")
if (( avif_size >= orig_size )); then
echo " AVIF larger than original. Removing: $rel_path"
rm -f "$avif_dest"
fi
fi
fi
webp_dest="$WEBP_DIR/${rel_base}.webp"
if [[ ! -f "$webp_dest" ]]; then
mkdir -p "$(dirname "$webp_dest")"
echo "Converting to WebP: $rel_path"
if ! "$CWEBP" -quiet "$src_img" -o "$webp_dest"; then
echo " WebP conversion failed: $rel_path"
rm -f "$webp_dest"
elif [[ -f "$webp_dest" ]]; then
orig_size=$(stat -c %s "$src_img")
webp_size=$(stat -c %s "$webp_dest")
if (( webp_size >= orig_size )); then
echo " WebP larger than original. Removing: $rel_path"
rm -f "$webp_dest"
fi
fi
fi
done
Breaking Down What the Script Does
The script looks simple, but there is a lot going on behind the scenes. Understanding it makes troubleshooting much easier later.
The first section defines your directories and checks for required tools. If avifenc or cwebp is missing, it exits immediately instead of failing silently. That alone saves a lot of time when something is not installed correctly.
The find command walks through your uploads directory and targets only .jpg, .jpeg, and .png files. Each file is processed one at a time, which keeps memory usage low even on large libraries. It also builds a relative path so the converted images mirror your original folder structure.
For each image, the script checks if a converted version already exists. If it does, it skips it, which prevents unnecessary work. If not, it converts the image and then compares file sizes.
That size check is important. If the converted image is larger than the original, it gets deleted immediately. This ensures you never accidentally make performance worse by converting images that do not benefit from compression.
Making the Script Executable
After placing the script in your server, make it executable.
chmod +x /home/user/path/to/scripts/convert_images/convert-to-avif-webp.sh
Without this step, cron will not be able to run it.
Automating with Cron
I run this script every 15 minutes. That keeps new uploads converted quickly without putting too much load on the server.
*/15 * * * * /home/user/path/to/scripts/convert_images/convert-to-avif-webp.sh
This setup works well because it is lightweight. The script skips files that are already processed, so each run only handles new images.
Serving Next Gen Images with Apache
At this point, the server is generating AVIF and WebP images in the background. The last piece is telling Apache how to serve those images automatically using your .htaccess file. This is where everything comes together, because it allows you to keep using your original image URLs while Apache quietly swaps in the optimized versions.
The .htaccess file sits in your web root, usually inside /home/user/public_html/. Apache reads this file on each request and applies the rewrite rules before serving content. That means you can control how images are delivered without touching your application code or WordPress itself.
Here is the full .htaccess configuration used for this setup:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_ACCEPT} image/avif
RewriteCond %{DOCUMENT_ROOT}/wp-content/avif-uploads/$1.avif -s
RewriteRule ^wp-content/uploads/(.+)\.(jpe?g|png)$ /wp-content/avif-uploads/$1.avif [T=image/avif,L]
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{DOCUMENT_ROOT}/wp-content/webp-uploads/$1.webp -s
RewriteRule ^wp-content/uploads/(.+)\.(jpe?g|png)$ /wp-content/webp-uploads/$1.webp [T=image/webp,L]
</IfModule>
<IfModule mod_headers.c>
Header append Vary Accept
</IfModule>
AddType image/avif .avif
AddType image/webp .webpThis configuration works by checking the browser’s Accept header. If the browser supports AVIF, Apache looks for a matching .avif file in your avif-uploads directory. If that file exists, it serves it instead of the original JPEG or PNG. If AVIF is not supported, it moves on and checks for WebP support, and then falls back to the original image if neither format is available.
The -s flag in the rewrite condition ensures the file exists and is not empty. This prevents broken image responses if a conversion failed or has not been created yet. It also keeps everything safe during the first few runs of your cron job while images are still being generated.
The Header append Vary Accept line is important for caching. It tells browsers and proxies that the response may change depending on the Accept header. Without this, you could run into caching issues where the wrong image format is served to the wrong browser.
The AddType directives make sure Apache correctly identifies AVIF and WebP files when serving them. Without these, some setups may send the wrong content type, which can cause display issues in certain browsers.
Once this is in place, everything happens automatically. You keep using standard image URLs like /wp-content/uploads/..., and Apache handles the rest behind the scenes.
Why This Approach Works Better Than Plugins
Most WordPress plugins try to do this in real time. They intercept requests, generate images on demand, and store them afterward. That works, but it adds overhead to every request.
This approach is different. Everything happens ahead of time. By the time a visitor requests an image, it is already converted and ready to go.
It also avoids dependency issues. You are not relying on an external API or a plugin that may stop being maintained. Everything runs locally using standard Linux tools.
Performance Benefits You Will Actually See
Once this is in place, the difference shows up quickly. Pages load faster, especially on mobile connections where bandwidth matters more. Image-heavy pages benefit the most, since those are usually the biggest contributors to load time.
Server load stays low because conversions happen in the background. Apache simply serves static files, which is what it does best. There is no additional processing during page requests.
Storage usage can increase slightly because you are keeping multiple versions of images. In practice, the savings in bandwidth and performance usually outweigh that tradeoff.
Final Thoughts
This setup gives you full control over image optimization without relying on third party services. It runs quietly, requires very little maintenance, and scales well as your site grows.
Once it is in place, you can forget about it. New images get converted automatically, Apache serves the best format available, and your site stays fast without extra effort.
If you are already managing your own server, this is one of those improvements that is worth the small amount of setup time.




















