# Convert your existing WordPress images to WebP without a plugin: what regenerate breaks

**Author:** SJ | **Published:** 2026-11-05 | **Tags:** WebP, JPEG, PNG | **URL:** https://www.iminify.com/blog/33-convert-your-existing-wordpress-images-to-webp-without-a-plugin-what-regenerate-breaks

---

On a WordPress 7.1.4 test site, one filter and `wp media regenerate` turned the resized copies WordPress makes of five full-colour (4:4:4) photographs into WebP files with 62.5% of the JPEG copies' bytes. All 25 WebP copies, five sizes of each photograph, also scored lower. The filter and `regenerate` also left four of our seven test posts pointing at copies WP-CLI had just deleted, and took the `srcset` off all seven inserted images that had one. The version below leaves no address broken and gives six of those seven their `srcset` back: the filter, `regenerate` with one flag, and a rewrite that changes an address only where a WebP exists. The recipe as printed needs WP-CLI and a shell on the server.

<!--more-->

We ran it on eleven images through WP-CLI: five photographs and four PNG graphics from Iminify's published showcase, a 5504 px Gemini picture from our own sample set, and a copy of one showcase photo with EXIF orientation 6 (`rot.jpg`). WordPress's [dev note](https://make.wordpress.org/core/2026/07/22/client-side-media-processing-in-wordpress-7-1/) says the browser honours the same filter, but we measured only the WP-CLI path.

## One filter makes WordPress save JPEG copies as WebP

WordPress picks each copy's file type with the `image_editor_output_format` filter, which has existed since 5.8 and whose default, since 6.7, maps only HEIC and HEIF to JPEG. Save this as `wp-content/mu-plugins/webp-output.php`, creating the folder if it isn't there:

```php
<?php
// wp-content/mu-plugins/webp-output.php
add_filter( 'image_editor_output_format', function ( $formats ) {
	$formats['image/jpeg'] = 'image/webp';
	return $formats;
} );
```

WordPress calls a file in that folder a [must-use plugin](https://developer.wordpress.org/advanced-administration/plugins/mu-plugins/) and enables it without activation, so "without a plugin" here means no other plugin to install.

New uploads now get WebP copies, and the full size becomes a WebP too: our 364,628-byte cafe photo got a 118,574-byte `photo-cafe.webp` beside it, plus five WebP copies. The upload stays on disk as the attachment's `original_image`. WordPress ignores the mapping if the server's image editor (Imagick or GD) can't write WebP, so check it first: `wp eval 'var_dump( wp_image_editor_supports( array( "mime_type" => "image/webp" ) ) );'` printed `bool(true)` on ours.

## Regenerate converts old images and deletes what posts point at

A filter acts on new files, so old images need `wp media regenerate`. WP-CLI calls the copies "thumbnails" and [deletes the old ones](https://developer.wordpress.org/cli/commands/media/regenerate/) unless you pass `--skip-delete`. Run it with the flag:

```bash
wp media regenerate --yes --skip-delete
```

Pick the WebP quality before this run (the quality section below has our numbers), because a later change means a second run.

The 43 WebP files regenerate wrote (36 copies and 7 full sizes) matched a fresh upload's files byte for byte.

| Address in the post | Default `regenerate` | `--skip-delete` | `--skip-delete` plus the rewrite below |
|---|---|---|---|
| Large copy of the fjord photo, block editor | 404 | JPEG, no `srcset` | WebP, 145,378 B (was 185,371), `srcset` |
| Medium copy of the cafe photo, classic editor | 404 | JPEG, no `srcset` | WebP, 9,434 B (was 14,104), `srcset` |
| Full-size upload of the raspberries photo | JPEG, no `srcset` | JPEG, no `srcset` | WebP, 101,066 B (was 350,425), `srcset` |
| Scaled full size of the 5504 px picture | JPEG, no `srcset` | JPEG, no `srcset` | WebP, 586,962 B (was 684,862), `srcset` |
| Featured image, shown by the theme | WebP, `srcset` | WebP, `srcset` | WebP, `srcset` |

Without the flag, a copy's address answered 404 in four of the seven posts: the two rows above, and the Large copy of the 5504 px picture and the Medium copy of the orientation-6 photo in their own posts. With the flag no address answered 404, but none inside a post's content got a WebP, and the inserted images lost their `srcset`. [WordPress's source](https://developer.wordpress.org/reference/functions/wp_calculate_image_srcset/) says why: "If no matches are found we don't return a srcset to avoid serving an incorrect image." The sizes in the attachment's data now end in `.webp`, so a `.jpg` address matches none of them.

## Rewrite addresses only where a WebP exists

Each copy keeps its name and changes only its extension, so each old copy has a WebP file of the same name. Back up the database, then save this as `webp-urls.php` in the WordPress folder:

```php
<?php
// webp-urls.php: point post content at the .webp copy wherever one exists
global $wpdb;
$up = wp_get_upload_dir();
foreach ( $wpdb->get_results( "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE '%/uploads/%'" ) as $row ) {
	$new = preg_replace_callback(
		'#(' . preg_quote( $up['baseurl'], '#' ) . '/[^"\'\s)]+)\.(?:jpe?g|png)\b#i',
		fn( $m ) => file_exists( $up['basedir'] . substr( $m[1], strlen( $up['baseurl'] ) ) . '.webp' ) ? $m[1] . '.webp' : $m[0],
		$row->post_content
	);
	if ( $new !== $row->post_content ) {
		echo "post {$row->ID}\n";
		if ( in_array( 'write', $args, true ) ) {
			$wpdb->update( $wpdb->posts, array( 'post_content' => $new ), array( 'ID' => $row->ID ) );
			clean_post_cache( $row->ID );
		}
	}
}
```

List the posts it would change, then save the changes, and delete the script when you're done:

```bash
wp eval-file webp-urls.php
wp eval-file webp-urls.php write
```

Six of the seven inserted images that had a `srcset` got WebP and a full `srcset` back. The seventh is the full size WordPress saved as `rot-rotated.jpg` when we uploaded `rot.jpg`: regenerate named its WebP `rot.webp`, so that address stays JPEG without a `srcset`.

The script looks for the file first because a blanket `search-replace` of `.jpg` to `.webp` also rewrote the address of a copy of the cafe photo we had put in `uploads/` without importing it, and the new address answered 404: a file the Media Library doesn't know gets no WebP. To convert one, use [our JPG to WebP converter](https://www.iminify.com/jpg-to-webp). To check what a live page loads after the rewrite, [scan it](https://www.iminify.com/scan).

The script reads `post_content` and the site's own upload URL only. Page-builder layouts in post meta, widgets, menus and SEO tables aren't covered, which is why we keep the old copies: whatever still points at them keeps working.

WP-CLI's nightly build (3.0.0-alpha on 2 November 2026; the stable release is 2.12.0) adds `--update-attachment-refs` to `regenerate`. Our run of it without `--skip-delete` deleted the old copies and moved the copies' addresses to WebP, but left the full-size addresses as JPEG without a `srcset`. The script above moves those too, where a WebP exists.

## The uploads folder grows, and the filter has to stay

Visitors download less after the rewrite, but disk use goes up, because the uploads stay and each full size gains a WebP beside it (seven files, 1.8 MB). Our uploads folder went from 23.1 MB to 23.8 MB with the default run, which also swaps the old copies (3.5 MB) for WebP ones (2.4 MB). With `--skip-delete`, which keeps the old copies too, it went to 27.3 MB.

Keep the filter. When we removed it after the rewrite and ran `wp media regenerate --yes`, WordPress wrote JPEG copies, WP-CLI deleted the WebP ones, and the four rewritten copy addresses answered 404. With `--yes --skip-delete` instead, nothing answered 404, but every image that had a `srcset` after the rewrite lost it, the featured image included, and only `rot-rotated.jpg` got one back.

## WebP at WordPress's default quality scored below the JPEG copies' median

WordPress saves JPEG copies at [quality 82 and WebP copies at 86](https://www.iminify.com/blog/10-does-wordpress-compress-images-what-wordpress-71-does-to-your-upload#every-jpeg-copy-is-re-encoded-at-quality-82). We scored each copy with SSIMULACRA 2 against a lossless reference of the same size, made with WordPress's own resize code. On [its scale](https://www.iminify.com/blog/1-ssimulacra-2-scores-in-practice-what-50-70-and-90-cost-in-kilobytes#the-ssimulacra-2-scale-is-a-set-of-viewing-tests), 70 is "high quality" and 80 "very high quality", where distortion isn't noticeable side by side.

There were two sets of 25 copies (five photographs at five sizes): the showcase JPEGs, which are full-colour (4:4:4), and the same photographs saved again as 4:2:0 JPEGs. Bytes are the WebP copies' total as a percentage of the JPEG copies' total, and the score is the median:

| WebP quality | 4:4:4 bytes | 4:4:4 score | 4:2:0 bytes | 4:2:0 score |
|---|---|---|---|---|
| JPEG at 82 | 100% | 83.98 | 100% | 80.72 |
| 75 | 38.0% | 69.70 | 48.1% | 69.92 |
| 80 | 47.1% | 74.52 | 59.3% | 75.03 |
| 86 (default) | 62.5% | 79.87 | 78.4% | 79.83 |
| 90 | 81.5% | 83.74 | 100.8% | 83.77 |
| 92 | 95.2% | 85.51 | 117.0% | 85.53 |
| 95 | 126.2% | 87.35 | 154.7% | 87.61 |

At 86, the WebP copy scored below its JPEG counterpart on all 25 copies of the first set and at or above it on 10 of 25 of the second. On the second set the JPEG's median falls between WebP 86 and 90; we didn't measure the qualities in between.

If most of your JPEGs are [4:2:0](https://www.iminify.com/blog/25-why-is-red-text-blurry-in-jpg-and-webp-chroma-subsampling-measured#which-tools-subsample-colour-until-you-say-otherwise), we'd keep 86: its median was 79.83 against the JPEG's 80.72, at 78.4% of the bytes. If most are 4:4:4, we'd set 90, within a quarter point of the JPEG (83.74 against 83.98) at 81.5% of the bytes. ImageMagick's `identify -format '%[jpeg:sampling-factor]\n' photo.jpg` prints `2x2,1x1,1x1` for 4:2:0 and `1x1,1x1,1x1` for 4:4:4. To set 90, add this to `webp-output.php`:

```php
add_filter( 'wp_editor_set_quality', function ( $quality, $mime ) {
	return 'image/webp' === $mime ? 90 : $quality;
}, 10, 2 );
```

Then run the `regenerate` command again. The rewrite needn't run twice: in our test it found nothing left to change.

Mapping `image/png` to `image/webp` converts PNG copies to lossy WebP as well. On our four graphics the 19 WebP copies weighed 22.9% of the PNG copies and scored 81.0 to 87.7, where the PNGs are lossless.

## How we measured

The site ran WordPress 7.1.4 from the `wordpress:7.1.4-php8.3-apache` image (PHP 8.3.36, ImageMagick 7.1.1) with WP-CLI 2.12.0, on 2 November 2026. We built the seven posts by script, the first two with the markup the block and classic editors write, and a request that followed no redirect read each address after each run. MB here is 1,000,000 bytes.

Each lossless reference is WordPress's own `make_subsize()` saved as PNG. Encoded again at WordPress's settings, the references reproduced the 50 stored WebP copies byte for byte, and the 25 JPEG copies of the 4:2:0 set. Encoded again, the references of the 4:4:4 set's 25 JPEG copies came out with the same pixels and length as the stored files, and three different header bytes. Scores come from `ssimulacra2` (libjxl 0.12.0), with alpha flattened onto white and WebP decoded by `dwebp` (libwebp 1.5.0). The 4:2:0 set was made with `djpeg` and `cjpegli -q 90 --chroma_subsampling=420`.
