Open almost any slow small-business website and look at what it downloads. The code is rarely the problem. The images are: a hero photo straight from a phone at 4000 pixels wide, a team portrait saved as PNG, a product shot that is the same file on a desktop monitor and on a phone over a train's patchy connection.
Nobody did anything wrong. The editor uploaded the photo they had, and the CMS showed it. But the visitor pays for it in load time, and Google measures that time and uses it when ranking pages.
What a good image actually needs
Every image on a page should answer three questions before it reaches the browser.
- Size. The file should be about as wide as the space it fills. A 390 pixel slot on a phone does not need 2400 pixels of image.
- Format. WebP is supported by every current browser and is usually much smaller than the same image as JPEG or PNG at the same visible quality.
- Crop. A wide banner on desktop and a tall card on mobile are different pictures. If the crop is left to the code, the subject's head ends up cut off.
Size and format are mechanical, and a server can handle them. Crop is a judgement call, and that is where most setups fall short.
Where Payload stops
Payload, the CMS I build most sites on, already resizes uploads with Sharp. You define imageSizes on the media collection, and every upload gets those versions. You can set a focal point so the automatic crop centres on the right spot.
That covers a lot, and it has two limits. First, every size is generated for every image, so a logo gets the same set of hero crops as a landscape photo. Second, the focal point is one point for all shapes. A point that frames a 16:9 banner well can still frame a 4:5 mobile card badly, and the editor has no way to fix one without moving the other.
The plugin I built
So I wrote payload-plugin-image-cropper. It is open source and on npm. The idea is simple: the developer defines the crops a field needs, and the editor draws each one in a crop window in the admin panel. When the document is saved, Sharp cuts and resizes the image on the server and stores the result next to the original.
A field for a blog cover with a wide desktop crop and a tall mobile one looks like this:
import { cropImageField } from "payload-plugin-image-cropper"
cropImageField({
name: "cover",
label: "Cover image",
crops: [
{ name: "desktop", label: "Desktop (16:9)", width: 1920, height: 1080, aspectRatio: 16 / 9 },
{ name: "mobile", label: "Mobile (4:5)", width: 828, height: 1035, aspectRatio: 4 / 5 },
],
})Each crop is saved as WebP at quality 80 unless you set another format or quality. Only the fields that need a crop get one, so a logo upload stays a logo upload.
One crop, several sizes
Responsive images need the same picture at a few widths, so the browser can pick the smallest one that still looks sharp. Asking an editor to crop three times for that would be absurd. Instead a crop can list sizes, and the plugin cuts every size from the one selection:
const cardCrop = {
name: "card",
label: "Card (16:9)",
aspectRatio: 16 / 9,
sizes: [
{ name: "lg", width: 1200, height: 675 },
{ name: "md", width: 768, height: 432 },
{ name: "sm", width: 390, height: 219 },
],
}On the frontend, getCropSrcSet turns that into a srcset, and the browser does the rest:
import { getCropSrcSet, getCropUrl } from "payload-plugin-image-cropper/utilities"
<img
src={getCropUrl(post.card, "card", "lg")}
srcSet={getCropSrcSet(post.card, cardCrop)}
sizes="(max-width: 768px) 100vw, 1200px"
alt={post.title}
/>The details that keep it working
A few smaller decisions matter more than they look.
- Crop file names are a hash of the source image and every crop setting. A re-crop writes a new file, so crops can be cached by the browser and a CDN for a year without anyone seeing a stale image.
- If an editor has not cropped yet,
getCropUrlfalls back to the original, andresolveMediaCropreturns the focal point as a CSSobject-position. The page never shows a broken image while content is still being prepared. - Crops are deleted when the source image is deleted, so storage does not fill up with orphans.
- It works with local files and with S3-compatible storage. For S3 you pass the same settings you already give
@payloadcms/storage-s3.
What this means if you run a website
You do not need to know any of the code above. What matters is that the people editing your site can upload whatever photo they have, choose how it is framed on a phone and on a laptop, and never think about file sizes. The site sends each visitor a file sized for their screen.
If your site feels slow, the images are the first place I would look. Run a page through PageSpeed Insights and check the opportunities it lists. If most of them mention images, the fix is usually less work than a redesign.