Клієнтська обрізка зображень: чому це важливо?
Більшість користувачів завантажують фотографії прямо з телефону: величезний оригінал 4000×3000 та 8 МБ. Сервер має прийняти, обробити та зберегти — навантаження на I/O та CPU зростає. Клієнтська обрізка вирішує це до відправлення: вирізаємо потрібний фрагмент, повертаємо, коригуємо яскравість — на сервер іде вже оптимізований файл. Ми використовуємо перевірені бібліотеки Cropper.js та react-image-crop, щоб вписати кроп у будь-який інтерфейс. Навіщо ганяти мегабайти, якщо можна відправити тільки те, що потрібно? Досвід 7+ років та 50+ проєктів з обробкою зображень підтверджує: такий підхід знижує навантаження на сервер у 3–5 разів та прискорює завантаження сторінок. Крім того, клієнтська обробка дозволяє заощадити до 70% коштів на зберігання та скоротити витрати на трафік у 5 разів. А ще це позитивно впливає на Core Web Vitals — особливо LCP, адже браузеру не потрібно чекати серверної обробки.
Які проблеми вирішує кроп на клієнті?
- Неправильні пропорції: користувачі завантажують горизонтальні фото для вертикальних аватарів — доводиться обрізати вручну.
- Величезна вага: вихідник 10 МБ перетворюється на 200 KB після кропу в WebP 0.9.
- EXIF-поворот: мобільні знімки відображаються перевернутими до виправлення метаданих.
- Зайві елементи: рекламні банери, водяні знаки — все це видаляється до завантаження.
- Завантаження на повільному з'єднанні: користувачі з 3G можуть чекати хвилини, а після кропу відправляється тільки 200 KB.
Як вибрати бібліотеку для кропу?
| Бібліотека | Розмір (gzip) | Фічі | Для яких задач |
|---|---|---|---|
| Cropper.js | ~34 KB | Поворот, зум, обрізка, аспект | Vanilla JS, jQuery, будь-який стек |
| react-image-crop | ~6 KB | Обрізка з аспектом, кастомний UI | React (18+, Next.js) |
| vue-cropper | ~15 KB | Обрізка, поворот, зум | Vue 2/3, Nuxt |
| Fabric.js | ~200 KB | Повноцінний редактор, шари, фільтри, текст | Якщо потрібен редактор з накладанням тексту/фільтрів |
Cropper.js у 2 рази швидше завантажується, ніж Fabric.js, для простого кропу. Якщо задача — тільки кадрування з фіксованим аспектом, react-image-crop легший у 5 разів.
Базова реалізація на React
import { useState, useRef, useCallback } from 'react'
import ReactCrop, { Crop, PixelCrop, centerCrop, makeAspectCrop } from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'
function centerAspectCrop(width: number, height: number, aspect: number): Crop {
return centerCrop(
makeAspectCrop({ unit: '%', width: 90 }, aspect, width, height),
width,
height
)
}
export function ImageCropUploader({ aspect = 1, onUpload }: {
aspect?: number
onUpload: (blob: Blob) => Promise<void>
}) {
const [imgSrc, setImgSrc] = useState('')
const [crop, setCrop] = useState<Crop>()
const [completedCrop, setCompletedCrop] = useState<PixelCrop>()
const imgRef = useRef<HTMLImageElement>(null)
function onSelectFile(e: React.ChangeEvent<HTMLInputElement>) {
if (e.target.files?.[0]) {
const reader = new FileReader()
reader.addEventListener('load', () => setImgSrc(reader.result?.toString() ?? ''))
reader.readAsDataURL(e.target.files[0])
}
}
function onImageLoad(e: React.SyntheticEvent<HTMLImageElement>) {
const { width, height } = e.currentTarget
setCrop(centerAspectCrop(width, height, aspect))
}
const getCroppedBlob = useCallback((): Promise<Blob> => {
const image = imgRef.current
if (!image || !completedCrop) throw new Error('No crop data')
const canvas = document.createElement('canvas')
const scaleX = image.naturalWidth / image.width
const scaleY = image.naturalHeight / image.height
canvas.width = completedCrop.width * scaleX
canvas.height = completedCrop.height * scaleY
const ctx = canvas.getContext('2d')!
ctx.drawImage(
image,
completedCrop.x * scaleX,
completedCrop.y * scaleY,
completedCrop.width * scaleX,
completedCrop.height * scaleY,
0, 0,
canvas.width,
canvas.height
)
return new Promise((resolve, reject) => {
canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('Canvas empty')), 'image/webp', 0.9)
})
}, [completedCrop])
async function handleSubmit() {
const blob = await getCroppedBlob()
await onUpload(blob)
}
return (
<div>
<input type="file" accept="image/*" onChange={onSelectFile} />
{imgSrc && (
<ReactCrop
crop={crop}
onChange={setCrop}
onComplete={setCompletedCrop}
aspect={aspect}
minWidth={100}
>
<img ref={imgRef} src={imgSrc} onLoad={onImageLoad} />
</ReactCrop>
)}
{completedCrop && (
<button onClick={handleSubmit}>Завантажити</button>
)}
</div>
)
}
Ключовий момент — масштабування через naturalWidth / width. Браузер відображає зображення в зменшеному вигляді, а вирізати потрібно з оригіналу.
Як реалізувати кроп за 5 кроків?
- Вибір бібліотеки залежно від фреймворку (таблиця вище).
- Завантаження файлу через
FileReaderта відображення в<img>. - Ініціалізація кроп-інтерфейсу з параметрами аспекту та мінімальним розміром.
- Отримання обрізаного фрагменту через
canvas.toBlob()у формат WebP з якістю 0.9. - Відправлення Blob через
FormDataна сервер.
Додатково: валідація розміру до кропу (обмеження 10 МБ), перевірка EXIF-орієнтації та попередній перегляд результату.
Cropper.js: поворот, зум та завантаження
<div>
<img id="image" src="/original.jpg">
<button onclick="cropper.rotate(90)">Повернути</button>
<button onclick="cropper.zoom(0.1)">+</button>
<button onclick="cropper.zoom(-0.1)">-</button>
<button onclick="uploadCropped()">Завантажити</button>
</div>
<script>
const cropper = new Cropper(document.getElementById('image'), {
aspectRatio: 16 / 9,
viewMode: 2, // не виходити за межі зображення
dragMode: 'move',
autoCropArea: 0.8,
restore: false,
guides: true,
center: true,
highlight: false,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: false,
})
async function uploadCropped() {
cropper.getCroppedCanvas({
maxWidth: 1920,
maxHeight: 1080,
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
}).toBlob(async (blob) => {
const fd = new FormData()
fd.append('image', blob, 'cover.webp')
await fetch('/api/upload', { method: 'POST', body: fd })
}, 'image/webp', 0.85)
}
</script>
Що робити з EXIF-орієнтацією?
Мобільні фото часто повернуті через метадані EXIF. Браузер до недавнього часу не враховував це автоматично. Ось як це виправити:
img {
image-orientation: from-image; /* підтримується в сучасних браузерах */
}
Для надійності використовуємо бібліотеку exifr:
import { parse } from 'exifr'
async function fixOrientation(file: File): Promise<string> {
const exif = await parse(file, { orientation: true })
return new Promise(resolve => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement('canvas')
const rotation = exif?.Orientation ?? 1
// застосовуємо трансформацію за таблицею EXIF
applyExifRotation(canvas, img, rotation)
resolve(canvas.toDataURL())
}
img.src = e.target!.result as string
}
reader.readAsDataURL(file)
})
}
Докладніше про властивість image-orientation читайте в MDN.
Валідація та обмеження
Завантажувати 20 МБ оригінал, щоб потім вирізати 200×200 — марнотратство. Перевіряємо перед показом кропу:
const MAX_SIZE_MB = 10
const MAX_DIMENSION = 4096
function validateImage(file: File): string | null {
if (file.size > MAX_SIZE_MB * 1024 * 1024) {
return `Файл занадто великий. Максимум ${MAX_SIZE_MB} МБ`
}
return null
}
// Для перевірки розмірів — тільки після завантаження в img
function checkDimensions(img: HTMLImageElement): string | null {
if (img.naturalWidth > MAX_DIMENSION || img.naturalHeight > MAX_DIMENSION) {
return `Зображення занадто велике. Максимум ${MAX_DIMENSION}px`
}
return null
}
Якщо потрібно прийняти великі файли — ресайзити до розумних розмірів перед кропом через canvas.drawImage зі зменшеними canvas.width/height.
Що входить в роботу
| Компонент | Опис |
|---|---|
| Вибір бібліотеки | Підбір оптимального рішення під ваш стек (React, Vue, vanilla) |
| Інтеграція кропу | Налаштування компонента з підтримкою аспекту, повороту, зуму |
| EXIF-корекція | Виправлення орієнтації мобільних знімків |
| Валідація | Перевірка розміру, розширення та типу файлу до обробки |
| Оптимізація | Стиснення в WebP з контрольованою якістю |
| Попередній перегляд | Живий перегляд результату до завантаження |
| Документація | Інструкція з інтеграції та підтримки |
Довірити це завдання професіоналам
Наш досвід: 7+ років у веб-розробці, 50+ проєктів з обробкою зображень на клієнті. Ми гарантуємо, що інтеграція буде виконана вчасно, код буде покритий тестами та документований. Отримайте консультацію нашого інженера — це безкоштовно та займе 30 хвилин. Замовте впровадження клієнтського кропу та скоротіть витрати на серверну інфраструктуру.
Підсумок: клієнтська обрізка знижує навантаження на сервер, покращує UX та економить трафік. Впровадьте її за 2–5 днів з нашою допомогою.







