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.







