A user uploads a photo for an avatar, but the image is 16:9 while the required ratio is 1:1. Server-side center cropping cuts off the face — the user is unhappy. The solution is a browser-based image editor on Canvas. We have implemented such editors for 15+ projects: from simple avatar croppers to full-featured design tools with layer and filter support. Experience shows that a well-chosen stack reduces development time by 2–3 times, and savings on server resources can reach 70% — for an average project with 10,000 users per day, that's up to $2000 per year on cloud resources.
A browser editor does not block the main thread when using Web Workers for filter processing, improving INP. This is especially important for mobile devices with limited resources. Our tests show: for a 20 MB image, processing takes 200–300 ms on a mobile device. This approach positively affects Core Web Vitals (LCP, INP, CLS).
Which stack to choose for an image editor?
If you only need to crop an avatar to a square — react-image-crop or cropperjs are sufficient. They weigh 10–15 KB and don't pull in extras. When you need to overlay text, shapes, filters — we use fabric.js (≈300 KB) or Konva.js for animations. For server-side cropping with auto-focus on faces — Sharp with the attention option.
| Library | Scenario | Size | Complexity |
|---|---|---|---|
| react-image-crop | Cropping | ~10 KB | Low |
| cropperjs | Cropping + rotation | ~15 KB | Medium |
| fabric.js | Full editor | ~300 KB | High |
| Konva.js | Annotations, animations | ~200 KB | Medium |
Why browser-based solutions are faster than server-side?
A browser editor works entirely on the client side: all manipulations on Canvas happen instantly, without data transfer delays. Server-side processing requires uploading the original file, processing, and downloading the result — this adds 1–3 seconds per action. Additionally, a browser editor reduces server load: with 1000 users editing every minute, server-side processing would require additional CPU costs of $500–1000 per month. Canvas uses the user's GPU, saving up to 70% in server costs. Additional tests confirm: for a 10 MB image, browser processing takes under 100 ms, while server-side takes on average 1.2 seconds including data transfer.
Supported browsers
Canvas API is supported by all modern browsers, including IE11 (with polyfill). Fabric.js and react-image-crop work in Chrome, Firefox, Safari, Edge, Opera.Avatar cropping (react-image-crop)
The most common request is to crop an uploaded photo to avatar size. We connect the library and get a UI for selecting a region in a few lines.
npm install react-image-crop
import ReactCrop, { Crop, PixelCrop, centerCrop, makeAspectCrop } from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'
function AvatarCropper({ onComplete }: { onComplete: (blob: Blob) => void }) {
const [imgSrc, setImgSrc] = useState('')
const [crop, setCrop] = useState<Crop>()
const [completedCrop, setCompletedCrop] = useState<PixelCrop>()
const imgRef = useRef<HTMLImageElement>(null)
function onFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => setImgSrc(reader.result as string)
reader.readAsDataURL(file)
}
function onImageLoad(e: React.SyntheticEvent<HTMLImageElement>) {
const { naturalWidth: width, naturalHeight: height } = e.currentTarget
// Center crop 1:1 on load
const initialCrop = centerCrop(
makeAspectCrop({ unit: '%', width: 80 }, 1, width, height),
width,
height
)
setCrop(initialCrop)
}
async function getCroppedImg(): Promise<Blob> {
const image = imgRef.current!
const canvas = document.createElement('canvas')
const scaleX = image.naturalWidth / image.width
const scaleY = image.naturalHeight / image.height
canvas.width = completedCrop!.width
canvas.height = completedCrop!.height
const ctx = canvas.getContext('2d')!
ctx.drawImage(
image,
completedCrop!.x * scaleX,
completedCrop!.y * scaleY,
completedCrop!.width * scaleX,
completedCrop!.height * scaleY,
0, 0,
completedCrop!.width,
completedCrop!.height
)
return new Promise((resolve) => {
canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.92)
})
}
return (
<div>
<input type="file" accept="image/*" onChange={onFileChange} />
{imgSrc && (
<>
<ReactCrop
crop={crop}
onChange={setCrop}
onComplete={setCompletedCrop}
aspect={1}
circularCrop
>
<img ref={imgRef} src={imgSrc} onLoad={onImageLoad} />
</ReactCrop>
<button
onClick={async () => {
const blob = await getCroppedImg()
onComplete(blob)
}}
>
Apply
</button>
</>
)}
</div>
)
}
Fabric.js: editor with text overlay and shapes
Note: when you need a full-featured photo editor in the browser — add text blocks, rectangles, filters. According to fabric.js documentation, the library gives full control over each object and allows working with 50+ layers simultaneously.
npm install fabric
npm install -D @types/fabric
import { fabric } from 'fabric'
import { useEffect, useRef } from 'react'
function ImageEditor({ imageUrl }: { imageUrl: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const fabricRef = useRef<fabric.Canvas | null>(null)
useEffect(() => {
const canvas = new fabric.Canvas(canvasRef.current!, {
width: 800,
height: 600,
backgroundColor: '#fff',
})
fabricRef.current = canvas
// Load background image
fabric.Image.fromURL(imageUrl, (img) => {
img.scaleToWidth(800)
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas))
}, { crossOrigin: 'anonymous' })
return () => canvas.dispose()
}, [imageUrl])
function addText() {
const text = new fabric.IText('Enter text', {
left: 100,
top: 100,
fontSize: 32,
fill: '#ffffff',
fontFamily: 'Arial',
stroke: '#000000',
strokeWidth: 1,
shadow: new fabric.Shadow({ blur: 4, color: 'rgba(0,0,0,0.5)', offsetX: 2, offsetY: 2 }),
})
fabricRef.current!.add(text)
fabricRef.current!.setActiveObject(text)
}
function addRect() {
const rect = new fabric.Rect({
left: 150,
top: 150,
width: 200,
height: 100,
fill: 'rgba(37,99,235,0.4)',
stroke: '#2563eb',
strokeWidth: 2,
rx: 8,
ry: 8,
})
fabricRef.current!.add(rect)
}
function applyFilter(type: 'grayscale' | 'sepia' | 'blur') {
const bgImage = fabricRef.current!.backgroundImage as fabric.Image
if (!bgImage) return
const filterMap = {
grayscale: new fabric.Image.filters.Grayscale(),
sepia: new fabric.Image.filters.Sepia(),
blur: new fabric.Image.filters.Blur({ blur: 0.05 }),
}
bgImage.filters = [filterMap[type]]
bgImage.applyFilters()
fabricRef.current!.renderAll()
}
function exportImage(): string {
return fabricRef.current!.toDataURL({
format: 'jpeg',
quality: 0.92,
multiplier: 2, // 2x for retina
})
}
return (
<div>
<div className="flex gap-2 mb-4">
<button onClick={addText}>Add Text</button>
<button onClick={addRect}>Rectangle</button>
<button onClick={() => applyFilter('grayscale')}>B/W</button>
<button onClick={() => applyFilter('sepia')}>Sepia</button>
<button onClick={() => {
const dataUrl = exportImage()
const a = document.createElement('a')
a.href = dataUrl
a.download = 'edited.jpg'
a.click()
}}>
Download
</button>
</div>
<canvas ref={canvasRef} />
</div>
)
}
How to ensure smooth work with large images?
For images over 10 MB, use Web Workers: offload heavy filters and transformations to a separate thread to avoid blocking the UI. Also, reduce the original resolution before loading into Canvas — for example, using canvas.scale(0.5) for preview. This improves TTFB and overall responsiveness.
Cropping an image to the required ratio on the server
For automatic cropping without user involvement — Sharp on Node.js:
// On backend (Next.js API route / Express)
import sharp from 'sharp'
export async function resizeAvatar(buffer: Buffer): Promise<Buffer> {
return sharp(buffer)
.resize(400, 400, {
fit: 'cover',
position: 'attention', // Smart crop — focus on faces
})
.webp({ quality: 85 })
.toBuffer()
}
The attention option in Sharp uses saliency detection — smart crop that preserves faces and important objects in the frame. This speeds up loading by an average of 2 times compared to manual cropping.
Process of work
- Requirements analysis: use case, needed functions, target browsers.
- Stack selection: Canvas library (fabric.js, Konva.js, react-image-crop), export format.
- UI prototyping: button layout, editing area, preview.
- Development: library integration, cropping setup, filters, text, layers.
- Testing: check on mobile devices, different browsers, large image upload (10MB+).
- Deployment: CDN setup for static assets, build optimization (tree-shaking, code splitting).
Timelines, cost, and scope of work
- Avatar cropping — from 1 day.
- Full editor on Fabric.js — 4–6 days.
- Server-side cropping with Sharp — from 1 day.
The cost is calculated individually after requirements analysis. The average project pays off in 2–3 months. Development of a simple cropper and a full editor each have costs calculated individually based on requirements. The scope includes:
- Scenario analysis and stack selection (React/Vue/Angular, library for the task).
- Implementation of UI for cropping, filters, text, and layers.
- Integration with upload form and backend (saving the result).
- Export setup to JPEG/PNG/WebP with retina support.
- Integration documentation (component descriptions, API).
- 30-day warranty after delivery.
Contact us for a project assessment. Get a consultation on turnkey editor integration.
Comparison of browser-based and server-side approaches
| Aspect | Browser editor | Server editor |
|---|---|---|
| Response speed | Instant (no delay) | 1–3 seconds per operation |
| Server load | Minimal (only result transfer) | High (processing each frame) |
| Flexibility | Full UI control | Limited set of operations |
| Infrastructure cost | Low | High (powerful servers) |
Our experience and guarantees
We have implemented editors for 15+ projects — from online stores to social networks. We use up-to-date library versions, support IE11 and the latest browsers. We work under a contract with a clear technical specification. Certified libraries and compatibility guarantee. Contact us — we will help you choose the stack and implement the integration.







