Most slow WordPress pages are slow because of images, and most of that weight is avoidable. The order of work is always the same. First stop uploading files far larger than any screen will show. Then make sure WordPress hands each visitor a sensibly sized copy through srcset. Then lazy-load what sits below the fold, but never the main image at the top. Only after that is it worth converting to WebP, which typically saves a quarter to a third of the bytes at the same visible quality. Whether you convert with a plugin or offline on your own machine depends on how many images you have, how often you add new ones and how much CPU your hosting plan gives you. This post covers each step, with the settings, filters and commands, and says plainly where a step does less than the marketing for it suggests.
Where the weight comes from#
Open the browser's network panel on a slow WordPress page and sort by size. The usual picture is a handful of images accounting for most of the transfer: a 4000-pixel photo from a phone used as a hero, a 2 MB PNG screenshot, a logo exported as a 900 KB file, a gallery of twenty full-size JPEGs scaled down by CSS. The browser downloads every byte even if it only paints 400 pixels of it.
There are three separate problems, and they need different fixes.
- Too many pixels. A 4000-pixel image displayed in a 700-pixel column carries about thirty times more pixels than it can show. Fix: resize.
- Too many bytes per pixel. A JPEG saved at quality 100 is several times larger than one at quality 80, with no difference you can see. Fix: compress, or use a better format.
- Too many images at once. A page with forty pictures does not need all forty before the visitor has scrolled. Fix: lazy loading.
Format conversion to WebP only attacks the second problem, and it is the last one to reach for. If you serve a 4000-pixel image as WebP you have a smaller file that is still far too big. How hosting affects TTFB and Core Web Vitals explains why the largest image is usually the element the Largest Contentful Paint score measures, which is why getting this right moves a number search engines look at. Server speed does not rescue it: a 3 MB image is 3 MB from any host.
What WordPress already does with an upload#
When you add a file to the Media Library, WordPress keeps the original and generates smaller copies. The default sizes are configured under Settings, then Media, and registered by themes and plugins in code.
| Size name | Default dimensions | Notes |
|---|---|---|
thumbnail | 150 x 150, cropped | Used in admin lists and some widgets |
medium | 300 x 300 max | Box, not a crop |
medium_large | 768 wide | Not shown in the Media settings screen |
large | 1024 x 1024 max | Common choice for content images |
1536x1536 | 1536 max | Added in 5.3 |
2048x2048 | 2048 max | Added in 5.3 |
Themes add more through add_image_size(), and every one of them multiplies the number of files per upload. Each upload can easily produce ten to twenty files on disk. That matters for storage and for conversion time, and it is the reason tools that process "the image" actually process a dozen files.
Since WordPress 5.3 there is also a ceiling. If an uploaded image is larger than 2560 pixels on its longest side, WordPress scales it down to that size, keeps your original on disk, and serves the scaled copy, whose name gains a -scaled suffix. This is controlled by a filter, and you can lower the threshold or turn it off:
// Lower the cap from 2560 to 1920 pixelsadd_filter( 'big_image_size_threshold', function () { return 1920;} );// Or disable the automatic scaling entirely// add_filter( 'big_image_size_threshold', '__return_false' );For almost every site 1920 is a better cap than 2560, because very few layouts display an image wider than that. Lowering it does not shrink what you uploaded in the past; it applies to new uploads only.
Two details that surprise people. WordPress recompresses JPEGs when it creates the sized copies, using a default quality of 82. That is a sensible default. And the original file stays on disk untouched, so the uploads folder is heavier than the sum of what visitors ever download.
Sizes: stop uploading what nobody can see#
The cheapest saving costs nothing but a habit. Before uploading, decide the largest width the image will ever be shown at, and export at roughly double that for high-density screens, no more.
| Where the image appears | Export width |
|---|---|
| Content column, 700 px wide | 1400 px at most |
| Full-width hero on desktop | 1920 px |
| Blog card thumbnail | 800 px |
| Logo | Use SVG, or 2x the display size |
Photographs go to JPEG or WebP. Screenshots and flat graphics with sharp edges often compress better as PNG or WebP lossless, and a PNG of a photograph is almost always a mistake worth fixing. A logo or icon should be an SVG; WordPress does not allow SVG uploads by default because an SVG can carry scripts, so enable it only through a plugin you trust and only for administrators.
If you are fixing an existing site rather than starting fresh, the numbers tell you where to start. List the largest files first:
$ cd wp-content/uploads$ find . -type f \( -name '*.jpg' -o -name '*.jpeg' -o -name '*.png' \) \ -size +500k -printf '%s %p\n' | sort -rn | head -20Anything over 500 KB in a content image is a candidate. Fix the top twenty by hand and you will usually have taken most of the weight off.
How srcset works, and why it needs the right sizes#
The srcset attribute lets the browser choose among several widths of the same image, and sizes tells it how wide the image will be displayed. WordPress adds both automatically to images inserted through the editor, built from the generated sized copies:
<img src="/wp-content/uploads/2026/10/cafe-1024x683.jpg" srcset="/wp-content/uploads/2026/10/cafe-300x200.jpg 300w, /wp-content/uploads/2026/10/cafe-768x512.jpg 768w, /wp-content/uploads/2026/10/cafe-1024x683.jpg 1024w, /wp-content/uploads/2026/10/cafe-1536x1024.jpg 1536w" sizes="(max-width: 1024px) 100vw, 1024px" width="1024" height="683" alt="A cafe interior" loading="lazy">The browser reads the sizes hint, multiplies by the screen's pixel density, and downloads the smallest candidate that is wide enough. A phone with a 390-pixel viewport at 3x density wants about 1170 pixels; a desktop with a 700-pixel column at 1x wants 700. The width descriptor (300w) is the real pixel width of the file, which is how the browser can compare candidates without downloading them.
This only helps if the candidates exist. Three common failures:
- The original is the only large copy. If a theme registers no sizes between 768 and 2560, a phone asking for 1170 pixels gets the 1536 one or the original. Adding a size at 1200 or 1600 helps, but only for new uploads (see regeneration below).
- `sizes` is wrong. WordPress guesses the display width from the size you inserted. If your theme shows the image in a narrow column, and
sizessays100vw, the browser downloads a larger candidate than it needs. Theme developers fix this with thewp_calculate_image_sizesfilter; as a site owner, choose a smaller inserted size or fix the theme. - Images hard-coded in theme templates or page builders. Output built from a raw URL rather than an attachment ID has no
srcset. Use the builder's image field, which stores the attachment, rather than pasting a URL.
Always keep width and height on the tag. WordPress adds them from the attachment metadata, and they let the browser reserve the space before the image arrives, which prevents the page from jumping as it loads. That is the largest single fix for layout shift, and it is already done unless a builder strips the attributes.
To see which sizes your site actually has, and to check a particular image, use WP-CLI if you have it:
$ wp media image-size$ wp post meta get 123 _wp_attachment_metadata --format=json | head -c 600The first command lists every registered size with its dimensions and whether it is cropped. The second shows what was generated for attachment 123.
Regenerating thumbnails after a change#
Add a size, change a crop, or lower a quality setting, and nothing changes for images already uploaded. The sized copies on disk were generated at upload time. To rebuild them you regenerate:
$ wp media regenerate --only-missing --yes--only-missing creates just the sizes that do not exist yet, which is fast and safe. Without it, WordPress rebuilds every size for every attachment, which on a library of several thousand images takes a long time and a lot of CPU, so run it in the quiet hours. Without shell access, use a regenerate plugin from the dashboard, and expect it to process in small batches.
Take a backup before a full regeneration. It replaces files, and a half-finished run on a small server can leave you with some posts pointing at sizes that were deleted. Backups that actually restore covers how to confirm the copy is usable before you rely on it, and the staging site workflow is a good place to run the first pass.
Also clean up after yourself. Unused sizes from an old theme sit on disk for ever. After switching themes, regenerating with the new set and removing the orphans can free gigabytes on a large library.
Lazy loading, and the one image that must not use it#
Lazy loading defers an image until it is near the visible area, so a long page does not download everything on arrival. Since WordPress 5.5 it is the default: the loading="lazy" attribute is added to images and iframes in content, with no plugin needed. Browsers support it natively, and the attribute is cheaper and more reliable than the JavaScript lazy-loaders that came before it.
The rule that matters is the opposite one. Never lazy-load the image at the top of the page. The hero or featured image is usually the Largest Contentful Paint element, and telling the browser to wait for it directly delays the thing being measured. Recent WordPress versions already avoid adding loading="lazy" to the first content image and add fetchpriority="high" to it, but themes and builders often override this, so check the rendered HTML rather than assuming.
The correct markup for the hero is plain, with a priority hint:
<img src="/wp-content/uploads/2026/10/hero-1600.webp" width="1600" height="900" alt="Product on a desk" fetchpriority="high" decoding="async">You can turn off the default for specific images with a filter, and the same filter system turns it off globally if a theme conflicts:
add_filter( 'wp_img_tag_add_loading_attr', function ( $value, $image, $context ) { // Return false to omit loading="lazy" for this tag if ( strpos( $image, 'hero-' ) !== false ) { return false; } return $value;}, 10, 3 );Anything below the first screen should be lazy: gallery thumbnails, logos in the footer, avatars in comments. Do not stack lazy loading with a lazy-loading plugin that rewrites the same tags; two systems competing is how images fail to appear at all. If images show as blank boxes until you scroll, or never load, switch off the plugin's lazy option and rely on the native attribute.
Background images set in CSS are not covered by loading="lazy". A hero built as a CSS background cannot be prioritised or lazy-loaded this way, which is one more reason to prefer a real img tag for important images.
WebP: what it gives and what it does not#
WebP is an image format from Google that supports lossy and lossless compression and transparency. Photographs typically come out roughly 25 to 35 percent smaller than the equivalent JPEG at similar visible quality, and a lossless WebP is usually smaller than the PNG it replaces. All current browsers display it, so you can treat it as a safe default and no longer need to worry about visitors who cannot view it. AVIF compresses further but takes much longer to encode; WordPress 6.5 and later accepts AVIF uploads, though few sites need it.
WordPress has accepted WebP uploads since version 5.8. You can drag a .webp file into the Media Library and it works like any other image: sized copies are created and srcset works. Whether sized copies can be generated depends on the image library on the server: the editor needs GD or Imagick built with WebP support. The Site Health screen under Tools, then Site Health, lists which are present. If it reports no WebP support, uploads of .webp files will work but resized versions will not be created.
It is worth being honest about the limits:
- WebP does not fix an oversized image. Resize first.
- Gains vary. A photograph saves a lot; an image that is already heavily compressed saves little; some tiny PNGs get larger.
- A converted copy is a second file per size. If you keep both, disk use rises before it falls.
- At very high quality settings the advantage shrinks. Quality 75 to 85 is the useful range for photographs.
Plugin or offline conversion#
There are two approaches, and the right one depends on the site.
| Plugin | Offline | |
|---|---|---|
| Effort | Install, configure, run | Convert, upload, relink |
| Server cost | CPU and PHP memory on your plan | None, uses your own machine |
| New uploads | Handled automatically | Manual or scripted each time |
| Best for | Blogs, shops, many editors | Fixed sets of images, portfolios |
A plugin
Image plugins such as ShortPixel, Imagify, EWWW Image Optimizer and Smush convert and compress on upload, bulk-process the existing library, and rewrite the markup so visitors with WebP support get it. Some do the work on your server using GD or Imagick; others send the file to the vendor's service and fetch the result, which saves your CPU but means your images leave your server and usually involves a quota or a paid plan. Read what a plugin actually does before enabling it, because the details differ and the marketing pages blur them.
Practical points for a plugin on a small hosting plan:
- Bulk conversion is where plans struggle. Converting a library of several thousand images is a burst of CPU and PHP memory. On RE:NODE the CPU limit is a hard throttle to the share you bought, so a bulk run on a small plan takes longer rather than failing, and a job that exceeds the memory limit makes the kernel stop the container, which then restarts clean instead of swapping. Set the plugin to small batches and let it run overnight.
- Check how the WebP is served. Plugins either rewrite the
imgtags to point at the WebP, wrap them in apictureelement, or rely on server rules that swap the file based on theAcceptheader. The first two work with any cache. The last needs server configuration and aVary: Acceptheader so a cache does not hand a WebP to a browser that cannot read it. - Keep your originals. Most plugins can keep a backup of the original; leave that on until you have looked at the results on a real page.
- One optimiser, not three. Running two image plugins on the same uploads means files compressed twice and rules fighting each other.
Offline conversion
If the image set is fixed, such as a portfolio, a menu or a product catalogue that changes monthly, convert on your own computer and upload the result. You control quality exactly, nothing runs on the server, and nothing is installed that can break after a plugin update.
With the cwebp tool from Google's libwebp package, or ImageMagick, a folder converts in one command:
$ for f in *.jpg; do cwebp -q 80 "$f" -o "${f%.jpg}.webp"; done# or with ImageMagick, also resizing to 1600 px wide at most$ mogrify -path out -format webp -quality 80 -resize '1600x>' *.jpgThe > in the resize argument means "only shrink larger images", so smaller files are left at their size. Check a few results by eye before running the whole library, and compare file sizes: if a WebP is not smaller than its source, keep the source.
Upload the results with the file manager or SFTP. RE:NODE's file manager takes folder uploads by drag and drop and previews images in the browser, which is enough to spot-check a batch. Files dropped straight into wp-content/uploads do not appear in the Media Library, because the library is a database table. For files that must show up in the editor, upload them through the Media Library itself. For files only referenced by the theme (backgrounds, logos), put them in the theme folder and reference them directly.
The hybrid that works for most people is to resize and compress by hand for the big images (heroes, banners), where quality matters most, and let a plugin deal with the stream of ordinary editor uploads.
Serving WebP with a fallback#
If you convert but still want to support a browser that cannot read WebP, the standard markup is picture, which lets the browser choose by format:
<picture> <source srcset="/wp-content/uploads/2026/10/cafe.webp" type="image/webp"> <img src="/wp-content/uploads/2026/10/cafe.jpg" width="1024" height="683" alt="A cafe"></picture>In 2026 the fallback is rarely necessary, and the simplest approach is WebP only. A plugin that offers a picture mode or an Accept header mode is solving a problem most sites no longer have, so choose the setting that leaves the least server configuration behind.
If you do serve by Accept header on an Apache-style server, a conversion rule looks like this, and it only works if a .webp sits beside each original:
<IfModule mod_rewrite.c>RewriteEngine OnRewriteCond %{HTTP_ACCEPT} image/webpRewriteCond %{REQUEST_FILENAME} \.(jpe?g|png)$RewriteCond %{REQUEST_FILENAME}.webp -fRewriteRule ^(.+)$ $1.webp [T=image/webp,E=WEBP:1,L]</IfModule><IfModule mod_headers.c>Header append Vary Accept env=REDIRECT_accept</IfModule>On nginx there is no .htaccess, so the same idea lives in the server configuration with a map on $http_accept. Check that the server sends image/webp as the content type for .webp files. Old mime.types files lack the entry, and a WebP served as application/octet-stream gets downloaded instead of displayed. Verify with curl:
$ curl -sI https://example.com/wp-content/uploads/2026/10/cafe.webp \ | grep -i -E '^(content-type|content-length|cache-control)'Then add a long cache lifetime to images, since they never change under the same file name. HTTP caching headers explained covers Cache-Control for static files, and WordPress caching explains why page cache and image optimisation solve different halves of the problem.
A worked example#
A shop has a product page with eight photos, each uploaded straight from a phone at 4032 by 3024 pixels and about 3.4 MB. WordPress scaled each to 2560 pixels, so the largest served file is around 1.4 MB, and the product grid shows them at 380 pixels wide.
- Set
big_image_size_thresholdto 1920 and register a 760-pixel size for the grid (double the 380-pixel display width). - Run
wp media regenerate --only-missing --yesto create the new size for existing products. - Re-export the eight hero images at 1600 pixels as WebP, quality 80, which gives roughly 120 to 200 KB each.
- Confirm
srcsetappears in the page source and that the first image is notloading="lazy". - Re-measure.
The page goes from roughly eight to eleven megabytes of images to well under two. Nothing in this list needed a plugin. The grid also lost most of its weight simply because the browser now picks the 760-pixel copy and not the 2560-pixel one.
Measuring that it worked#
Do not trust the plugin's own percentage claim. Measure the page before and after with the same tool: the browser's network panel (disable cache, then read the transferred size for images), Lighthouse, or PageSpeed Insights. Look at three figures: total image bytes, the LCP element and its load time, and layout shift.
From the command line, check what a particular visitor would receive:
$ curl -s -o /dev/null -w '%{size_download} bytes\n' \ -H 'Accept: image/webp,*/*' \ https://example.com/wp-content/uploads/2026/10/cafe.jpgRun it once with the Accept header and once without. If the byte counts differ, server-side negotiation is working; if they match, you are serving the original to everyone. Remember that a CDN or page cache in front of the site may hold an old version of the page, so purge it after a change or you will measure yesterday's HTML.
A short checklist for any image problem:
- Is the file larger than its displayed size by a factor of two or more?
- Does the tag have
srcset,widthandheight? - Is the top image free of
loading="lazy"? - Is the format right for the content (photo, graphic, logo)?
- Is a long
Cache-Controlset for the file type?
Hosting notes#
Image work is mostly front-end, and no hosting plan makes up for oversized files. What the host does decide is how comfortable the mechanics are. On a RE:NODE WordPress plan you can edit .htaccess or wp-config.php in the file manager, upload folders over SFTP, and take a backup from the Backups tab before a bulk run. Backup slots vary by plan, so check the plan page for the number before you rely on one. Disk space is the other limit worth watching: every upload multiplies into a dozen files, and a plugin that keeps originals alongside WebP copies will use more, not less, until you prune. If a large library is the problem, offloading WordPress media to S3 moves it off the web server's disk, at the cost of one more moving part.
FAQ#
Is WebP better than JPEG for WordPress?
For photographs it is usually 25 to 35 percent smaller at the same visible quality, and every current browser reads it. It does not help if the image is the wrong size to begin with, so resize first and convert second.
Do I need a plugin to use WebP?
No. WordPress has accepted WebP uploads since 5.8, and you can convert offline with cwebp or ImageMagick and upload the files. A plugin makes sense when many people upload and you want conversion to happen automatically.
Does lazy loading hurt SEO?
Native loading="lazy" is read correctly by search engines, so images below the fold still index. The harm comes from lazy-loading the main image at the top, which delays Largest Contentful Paint. Leave the first image eager.
Why are my images still large after optimising?
Usually the page is using the original or the -scaled copy because srcset is missing, the theme registered no mid-size copies, or the image comes from a CSS background. Check the rendered HTML for the srcset attribute and the network panel for the file actually downloaded.
Do I need to regenerate thumbnails after changing a theme?
Yes, when the new theme uses different image sizes. Otherwise it asks for sizes that do not exist and falls back to a larger one. Run wp media regenerate --only-missing --yes and take a backup first.
Can optimisation damage my images?
Lossy compression discards data and cannot be undone, so keep the originals until you have checked the result on real pages. Never overwrite your only copy with a compressed one.


