6.9 KiB
6.9 KiB
Design Spec: Lighthouse Optimization (~100)
Date: 2026-05-15
Status: Draft — awaiting user review
Problem
Lighthouse score далеко от 100 из-за:
- Изображения загружаются в полном размере (7–15 МБ файлы для карточек 200px)
- Нет адаптивных изображений (srcset/sizes)
- Нет lazy loading на большинстве изображений
- Нет Cache-Control для
/uploads/ - JS бандл 2.3 МБ без code splitting
- Минимальные meta-теги в HTML
Architecture
1. Серверный ресайз изображений
Интеграция с Fastify
Регистрируем маршрут GET /uploads-resized/* ПЕРЕД fastify-static:
- Если запрошен
.avifили.webpс?w=→ sharp конвертирует - Если запрошен оригинал (без
?w=или с неподдерживаемым форматом) → редирект на/uploads/... fastify-staticостаётся для прямой отдачи/uploads/(обратная совместимость)
URL формат: /uploads-resized/:relativePath/:filename.:format?w=WIDTH
:relativePath— опциональная подпапка (напримерreviews/):filename— UUID оригинала:format—avifилиwebp?w=— целевая ширина (320, 640, 1024, 1600)
Примеры:
/uploads-resized/abc123.avif?w=640— AVIF, 640px/uploads-resized/reviews/def456.webp?w=320— отзыв, WebP, 320px
Логика:
- Парсим URL → находим оригинал в
/uploads/:relativePath/:filename.* - Проверяем кеш:
/uploads/.cache/:relativePath/:filename_w{WIDTH}.{format} - Если кеш есть — отдаём с
Cache-Control: public, max-age=31536000, immutable - Если нет — sharp конвертирует, сохраняет в кеш, отдаёт
- Без
?w=— отдаёт оригинал сCache-Control: public, max-age=86400
Зависимость: sharp (npm package)
Разделение папок
| Источник | Путь сохранения |
|---|---|
| Админка (товары, галерея) | /uploads/ (без изменений) |
| Отзывы | /uploads/reviews/ |
Изменения:
server/src/routes/api/public-reviews.js—POST /api/reviews/upload-image→saveImageBufferToUploadsв/uploads/reviews/server/src/lib/upload-images.js—saveImageBufferToUploadsпринимает опциональныйsubdirпараметр
2. Клиент: <OptimizedImage> компонент
Расположение: client/src/shared/ui/OptimizedImage.tsx
Props:
type OptimizedImageProps = {
src: string // оригинальный URL (/uploads/...)
alt: string
widths?: number[] // по умолчанию [320, 640, 1024, 1600]
sizes?: string // HTML sizes attribute
aspectRatio?: string // CSS aspect-ratio
sx?: SxProps<Theme> // MUI sx
priority?: boolean // true = loading="eager", false = "lazy"
isReview?: boolean // true = путь /uploads/reviews/
}
Генерация srcset:
/uploads-resized/abc.avif?w=320 320w,
/uploads-resized/abc.avif?w=640 640w,
/uploads-resized/abc.avif?w=1024 1024w,
/uploads-resized/abc.avif?w=1600 1600w
Fallback для Safari без AVIF: <picture> с <source type="image/avif"> + <img type="image/webp">
Места замены:
ProductCard— карточки товаров (mediaHeight определяет нужный size)ProductPage— страница товара + отзывыCatalogSlider— слайдер каталогаGalleryGrid— галерея админкиAdminProductsPage/AdminPage— превью в админкеGallerySliderSection— превью слайдов
3. Cache-Control для статики
Server (server/src/index.js):
/uploads/(оригиналы) →Cache-Control: public, max-age=86400/uploads-resized/→Cache-Control: public, max-age=31536000, immutable/uploads/.cache/→Cache-Control: public, max-age=31536000, immutable
4. Code splitting
Vite (client/vite.config.ts):
manualChunks:
- vendor-react: react, react-dom, react-router-dom
- vendor-mui: @mui/*, @emotion/*
- vendor-swiper: swiper
- vendor-map: maplibre-gl, react-map-gl
- vendor-tiptap: @tiptap/*
- vendor-effector: effector, effector-react
- vendor-query: @tanstack/react-query
Route-level lazy loading:
/admin/*→React.lazy()— админка не нужна обычным пользователям/me/*→React.lazy()— личный кабинет
5. HTML meta оптимизации
client/index.html:
<meta name="description" content="..."><link rel="preconnect" href="...">для внешних ресурсов- OG tags (title, description, image)
- Canonical URL
Data Flow
[Browser] → GET /uploads-resized/abc.avif?w=640
↓
[Fastify route /uploads-resized/*] → файл есть в .cache?
↓ NO
→ читаем оригинал /uploads/abc.{png|jpg|webp}
→ sharp конвертирует в AVIF, width=640
→ сохраняем в /uploads/.cache/abc_w640.avif
↓ YES
→ читаем из .cache
↓
→ отдаём с Cache-Control: public, max-age=31536000, immutable
[ProductCard] → <OptimizedImage src="/uploads/abc.png" widths={[320,640,1024]} />
↓
[Browser] → выбирает нужный размер из srcset по viewport
↓
[Server] → отдаёт AVIF из кеша или генерирует
Error Handling
- Оригинал не найден → 404
- Sharp недоступен (не установлен) → fallback на оригинал + warn в лог
- Конвертация失败 → fallback на оригинал
- Невалидный
?w=→ игнорировать, использовать оригинал
Testing
- Server: тест ресайза — файл создаётся, формат правильный, размер соответствует
- Server: тест кеша — повторный запрос не вызывает повторную конвертацию
- Client: OptimizedImage рендерится с правильным srcset
- Client: fallback для non-AVIF браузеров
Migration Notes
- Старые файлы в
/uploads/остаются без изменений - Новые отзывы →
/uploads/reviews/ - Конвертация происходит on-demand при первом запросе
.gitignore:/uploads/.cache/