Compress Images Before You Upload: A Faster Website Workflow
You run Lighthouse on a page you just shipped, and there it is again, right at the top of the report: "Properly size images." Underneath, a list of offenders, led by a 4.2 MB hero photo that someone exported straight fro
You run Lighthouse on a page you just shipped, and there it is again, right at the top of the report: "Properly size images." Underneath, a list of offenders, led by a 4.2 MB hero photo that someone exported straight from a camera and dropped into the CMS.
Images are usually the heaviest thing on a web page, and they're often the Largest Contentful Paint element too, which means they directly affect how fast your page feels and how it scores in Core Web Vitals. The fix isn't complicated, but it does need a consistent workflow, because "remember to compress it" is not a process.
So if you've been searching for how to compress images for a website properly, this is the workflow I use. It's six steps, a short script that does most of it in one command, and the HTML to serve the results the right way.
Disclosure: I work with the team behind SEO Site Checker, whose image compressor I mention below. The rest of the workflow uses open-source tools.
The Workflow in 60 Seconds
- Resize to the largest size you actually display.
- Choose the right format for the job.
- Compress with sensible quality settings.
- Strip metadata you don't need.
- Automate it, so it happens every time.
-
Serve it properly with
<picture>,srcsetand the right loading hints.
Why Compress Before Upload, Not After
Most CMS platforms and plugins can optimize images after you upload them. That's better than nothing, but it has real downsides:
- The original still lands on your server. It gets stored, backed up and sometimes served, depending on how the theme handles image sizes.
- Big files bloat your Git repo forever. If your images live in the repo, a 6 MB PNG committed once stays in your history permanently, even after you replace it.
- You give up control. Automatic optimizers apply one-size-fits-all settings. Some are too aggressive and leave visible artifacts. Others barely touch the file.
- Results are inconsistent. Different people, different tools, different outcomes.
Compressing before upload means every image arrives at the right size, in the right format and at the right quality, whether it's going into a CMS, a static site or a Git repo.
Step 1: Resize to the Size You Actually Display
Resizing is the biggest single win, and it's the step people skip most often. A modern phone photo is often 4,000 pixels wide. If it displays in an 800-pixel column, roughly 96% of those pixels are thrown away by the browser after being downloaded.
A few rules of thumb:
- Export at the width you display, or up to 2x that width for important images on high-density screens.
- Cap hero images at around 1,600 to 2,000 pixels wide. Larger rarely looks better, but it's always heavier.
-
Generate a few widths (for example 480, 960 and 1600) so the browser can pick the right one with
srcset. More on that in Step 6.
The reason 2x isn't a free lunch is that file size grows with pixel count, not width. As web.dev's guide to choosing the right image format points out, doubling an image's resolution quadruples its pixels. So use 2x where it matters, not everywhere.
With sharp, resizing is one line:
import sharp from 'sharp';
await sharp('photo.jpg')
.rotate() // apply EXIF orientation before metadata is removed
.resize({ width: 1600, withoutEnlargement: true })
.toFile('photo-1600.jpg');
Step 2: Pick the Right Format
Format choice matters almost as much as compression settings. Here's the short version:
| Format | Best for | Notes |
|---|---|---|
| JPEG | Photos | Universal support. The safe fallback. |
| PNG | Screenshots, graphics with sharp edges, transparency | Lossless, so files are large. Don't use it for photos. |
| WebP | Almost everything raster | Smaller than JPEG and PNG, supports transparency. |
| AVIF | Photos and complex images | Often smaller than WebP. Slower to encode. |
| SVG | Logos, icons, simple illustrations | Vector, so sharp at any size and usually tiny. |
| GIF | Nothing, really | Use a short video file for animation instead. |
WebP is the easy default today. Google's WebP documentation reports that lossy WebP images are 25 to 34% smaller than comparable JPEGs at equivalent quality, and lossless WebP images are 26% smaller than PNGs. It's supported in Chrome, Safari, Firefox, Edge and Opera.
AVIF can go smaller still, and it's supported by current versions of all the major browsers. The trade-offs are slower encoding and, at very low quality settings, some color shifts. Serving AVIF with a WebP or JPEG fallback (Step 6) gives you the best of both.
Step 3: Compress With Sensible Settings
First, a quick reality check on the phrase you'll see everywhere: "compress without losing quality." For PNG and lossless WebP, that's literally true. Lossless compression just stores the same pixels more efficiently.
For JPEG, lossy WebP and AVIF, compression permanently discards some image data. There is some quality loss. The goal is to make that loss invisible to the human eye while cutting the file size dramatically. That's very achievable with the right settings.
Good starting points:
- JPEG: quality 75 to 85. Use MozJPEG if your tool supports it, as it squeezes out extra savings.
- WebP: quality 75 to 85.
- AVIF: around 50 to 65. AVIF's quality scale isn't the same as JPEG's, so a lower number doesn't mean worse-looking.
Always compare the result against the original at 100% zoom, especially on faces, text and smooth gradients, which show artifacts first.
For tools, pick whatever fits the situation:
- One-off images: a free online image compressor handles PNG, JPEG and WebP files in a few seconds, with no settings to fiddle with. It's handy when someone sends you a single image and you just need it smaller.
- Visual comparison: Squoosh, Google's browser-based app, shows the original and compressed versions side by side with a slider, which is great for finding the right quality level.
- Scripting: command-line tools work well in build scripts:
# WebP at quality 80
cwebp -q 80 input.jpg -o output.webp
# AVIF at quality 60
avifenc -q 60 input.jpg output.avif
# Resize, strip metadata and recompress a JPEG with ImageMagick
magick input.jpg -resize 1600x -strip -quality 80 output.jpg
One gotcha: recompressing a JPEG that's already been compressed often saves little or nothing, and can even produce a bigger file. If a tool reports zero savings, it's usually because the image was already optimized. Always work from the original where you can.
Step 4: Strip Metadata
Photos carry metadata, known as EXIF data: camera model, lens settings, timestamps and, very often on phones, GPS coordinates. Publishing a photo taken at someone's home can publish where they live. Stripping metadata removes that risk and shaves off some bytes.
Two things to keep in mind. First, keep or convert the color profile, ideally to sRGB, so colors look the same in every browser. Second, apply the EXIF orientation before stripping, or portrait photos can suddenly appear sideways. sharp removes metadata by default, and its .rotate() method (with no arguments) applies the orientation first, which is why it appears in every example here.
Step 5: Automate It With a Pre-Upload Script
Doing all of this by hand works for one image. For a whole site, put it in a script. Here's the one I use. It takes a folder of original JPEG and PNG files and, for each one, writes several widths in AVIF, WebP and JPEG, with metadata stripped, then prints a before-and-after report.
// scripts/optimize-images.mjs
// Usage: npm install sharp && node scripts/optimize-images.mjs
import sharp from 'sharp';
import { readdir, mkdir, stat } from 'node:fs/promises';
import path from 'node:path';
const SRC = 'images-src'; // put your original images here
const OUT = 'public/images'; // optimized images go here
const WIDTHS = [480, 960, 1600];
await mkdir(OUT, { recursive: true });
const files = (await readdir(SRC)).filter((f) => /\.(jpe?g|png)$/i.test(f));
for (const file of files) {
const input = path.join(SRC, file);
const name = path.parse(file).name;
const { size: originalBytes } = await stat(input);
const { width: originalWidth } = await sharp(input).metadata();
// Never upscale: keep only widths the original can support
const widths = WIDTHS.filter((w) => w <= originalWidth);
if (widths.length === 0) widths.push(originalWidth);
let largestWebp = 0;
for (const width of widths) {
const base = sharp(input).rotate().resize({ width });
await base.clone().avif({ quality: 55 }).toFile(path.join(OUT, `${name}-${width}.avif`));
const webp = await base.clone().webp({ quality: 80 }).toFile(path.join(OUT, `${name}-${width}.webp`));
await base.clone().jpeg({ quality: 80, mozjpeg: true }).toFile(path.join(OUT, `${name}-${width}.jpg`));
largestWebp = Math.max(largestWebp, webp.size);
}
const kb = (bytes) => `${(bytes / 1024).toFixed(1)} KB`;
console.log(`${file}: ${kb(originalBytes)} original, largest WebP ${kb(largestWebp)}`);
}
Add it to your package.json so anyone on the team can run it:
{
"scripts": {
"images": "node scripts/optimize-images.mjs"
}
}
To stop oversized images from sneaking back in, add a size budget check to CI or a pre-commit hook. This one (Node 20 or newer) fails if any image in the output folder is over 300 KB:
// scripts/check-image-budget.mjs
import { readdir, stat } from 'node:fs/promises';
import path from 'node:path';
const DIR = 'public/images';
const BUDGET_KB = 300;
let failed = false;
for (const file of await readdir(DIR, { recursive: true })) {
if (!/\.(avif|webp|jpe?g|png|gif)$/i.test(file)) continue;
const { size } = await stat(path.join(DIR, file));
if (size > BUDGET_KB * 1024) {
console.error(`${file} is ${Math.round(size / 1024)} KB (budget: ${BUDGET_KB} KB)`);
failed = true;
}
}
process.exit(failed ? 1 : 0);
The budget number is a guideline, not a law. A full-width hero image might reasonably sit at 200 to 300 KB, while thumbnails should be a fraction of that. Adjust it to your site. The sharp documentation covers every option if you want to tune the output further.
Step 6: Serve Images Properly
Compression gets you halfway. The other half is making sure the browser downloads the right file at the right time. Here's a hero image using the files the script produced:
<picture>
<source type="image/avif"
srcset="/images/hero-480.avif 480w, /images/hero-960.avif 960w, /images/hero-1600.avif 1600w"
sizes="(max-width: 1000px) 100vw, 1000px">
<source type="image/webp"
srcset="/images/hero-480.webp 480w, /images/hero-960.webp 960w, /images/hero-1600.webp 1600w"
sizes="(max-width: 1000px) 100vw, 1000px">
<img src="/images/hero-960.jpg"
srcset="/images/hero-480.jpg 480w, /images/hero-960.jpg 960w, /images/hero-1600.jpg 1600w"
sizes="(max-width: 1000px) 100vw, 1000px"
width="1600" height="900"
alt="Team reviewing a website design on a large monitor"
fetchpriority="high">
</picture>
A few details make a big difference:
-
The browser picks the first format it supports, so list AVIF, then WebP, with JPEG as the fallback in the
<img>. -
srcsetandsizeslet the browser choose the right width for the screen, so phones don't download desktop-sized images. -
Always set
widthandheight. The browser uses them to reserve space before the image loads, which prevents layout shift (CLS). -
Don't lazy-load the hero image. It's usually your LCP element, so it should load immediately, and
fetchpriority="high"tells the browser to prioritize it. -
Do lazy-load images further down the page with
loading="lazy"anddecoding="async":
<img src="/images/gallery-960.webp" width="960" height="640"
alt="Finished kitchen renovation with oak cabinets"
loading="lazy" decoding="async">
Framework and Platform Shortcuts
If you're on a modern framework, some of this is built in. Next.js's <Image> component and Astro's <Image /> both resize, convert formats and add srcset automatically. Image CDNs can transform images on the fly through URL parameters.
They're great, and you should use them. But it's still worth compressing your source files first. Smaller sources mean a lighter repository, faster builds and less work at request time.
A Simple Version for Content Editors
Scripts are great for developers, but on most teams, the people uploading images are marketers, writers and clients working in a CMS. If that's your situation, give them a simpler version of the workflow. Here's a checklist you can paste into your team's docs on how to compress images for a website before uploading:
- Resize first. Crop and resize the image to roughly the width it will appear on the page. For a blog post body, around 1,200 to 1,600 pixels wide is usually plenty. Most laptops have a built-in tool for this, such as Preview on a Mac or Photos on Windows.
- Compress it. Run it through an online compressor, and check the result looks right before you upload.
-
Use a sensible file name. Rename
IMG_4823.jpgto something descriptive, likeoak-kitchen-cabinets.jpg. It helps image search and keeps the media library tidy. - Write alt text in the CMS. Describe what the image shows, in a short sentence, for people using screen readers.
- Never upload straight from a phone or camera. Those files are almost always far bigger than a website needs.
It takes an extra minute per image, and it prevents the 4 MB hero photo problem entirely. If you can, back it up with the size budget check from Step 5, so anything that slips through gets caught before it goes live.
How to Check Your Results
After deploying, verify it worked:
- Lighthouse and PageSpeed Insights flag image problems directly. Look for "Properly size images," "Serve images in next-gen formats" and "Efficiently encode images."
- The DevTools Network panel shows exactly what's downloaded. Filter by "Img," sort by size and check which format and width each image actually served.
- A site-wide check catches oversized images on pages you haven't looked at in a while. A quick site-wide SEO audit flags heavy images alongside other performance and SEO issues.
Common Mistakes
- Recompressing the same JPEG over and over instead of working from the original.
- Lazy-loading the hero image, which delays LCP.
- Using PNG for photos.
- Leaving out
widthandheight, which causes layout shift. - Shrinking giant images with CSS instead of resizing the file.
- Serving AVIF or WebP with no fallback for older clients.
- Exporting every screenshot at full Retina resolution, even thumbnails.
FAQs
How do I compress images without losing quality?
Use lossless compression (PNG or lossless WebP) when the image must be pixel-perfect. For photos, use lossy JPEG, WebP or AVIF at moderate quality settings, around 75 to 85 for JPEG and WebP. Some data is discarded, but at those settings the difference is usually invisible.
What is the best image format for websites?
For most photos and graphics, WebP is the best all-round choice, with AVIF as an even smaller option where you can provide a fallback. Use SVG for logos and icons, JPEG as a universal fallback, and PNG only when you need lossless quality or transparency.
What file size should website images be?
There's no single rule, but as a guideline, aim for most content images to be under 100 to 200 KB and large hero images under about 300 KB. Thumbnails should be far smaller. Resizing to the displayed dimensions usually does more than compression alone.
What dimensions should images be for a website?
Match the largest size the image will actually display, and go up to 2x for important images on high-density screens. For full-width hero images, around 1,600 to 2,000 pixels wide is plenty. Generate several widths and let srcset serve the right one.
Does compressing images help SEO?
Indirectly, yes. Smaller images make pages load faster, which improves user experience and Core Web Vitals, especially Largest Contentful Paint. Google uses page experience as one of many signals. Descriptive file names and alt text also help your images appear in image search.
What's the difference between lossy and lossless compression?
Lossless compression stores exactly the same pixels more efficiently, so there's no quality loss, but savings are smaller. Lossy compression permanently discards some data, producing much smaller files. At sensible settings, lossy compression is usually indistinguishable from the original to the human eye.
Wrapping Up
Resize first, pick the right format, compress with sensible settings, strip the metadata, then serve the result with <picture>, srcset and proper loading hints. Put the whole thing in a script and a CI check, and "properly size images" disappears from your Lighthouse reports for good. For anything outside that workflow, there's a collection of other free web tools for quick one-off jobs.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.