14 KiB
Кейс-стади: Мобильная оптимизация лендинга Никиты Тарасова
Production-кейс: лендинг ~2300 строк, 100KB, одиночный HTML-файл → улучшенная мобильная версия. 20+ мобильных улучшений.
Исходные данные
- Тип: Персональный лендинг (один HTML-файл, без сборщика)
- Объём: ~2325 строк, ~101KB
- Стек: Чистый HTML + CSS + минимальный vanilla JS, Tailwind CDN
- Проблема: Базовые
@media (max-width: 768px)медиа-запросы, нет safe-area, нет sticky CTA, нет prefers-reduced-motion, нет touch-оптимизаций
Категории улучшений
1. 🏠 Viewport & Safe Area (iOS)
Проблема: 100vh обрезает контент на iOS
На iPhone Safari 100vh включает область адресной строки, которая при скролле сжимается. Контент внизу обрезается.
Исправление:
<!-- Было -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Стало -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
/* Для секции hero */
#hero {
min-height: 100dvh; /* dynamic viewport height — учитывает/не учитывает UI браузера */
}
/* Для контейнера — safe-area */
@supports (padding: max(0px)) {
.container {
padding-left: max(24px, env(safe-area-inset-left));
padding-right: max(24px, env(safe-area-inset-right));
}
body {
padding-bottom: max(0px, env(safe-area-inset-bottom));
}
}
/* Для фиксированной навигации */
.nav {
padding-top: max(0px, env(safe-area-inset-top));
}
Зачем: Без viewport-fit=cover safe-area инсеты игнорируются. Без dvh на iPhone 15 Pro с Dynamic Island hero-секция обрезается на ~50px.
2. 📱 Sticky Bottom CTA (мобильная конверсия)
Проблема: на мобильном CTA «внизу» скрыты в конце длинной страницы. Пользователь должен пролистать обратно.
Исправление:
.sticky-cta {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px;
padding-bottom: max(12px, env(safe-area-inset-bottom));
background: rgba(10, 10, 11, 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid var(--border);
z-index: 90;
display: none; /* desktop: hidden */
flex-direction: column;
gap: 4px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
}
/* Показываем только на мобильных */
@media (max-width: 768px) {
.sticky-cta { display: flex; }
main { padding-bottom: 80px; } /* место под sticky CTA */
}
<div class="sticky-cta">
<a href="#contact" class="btn btn-primary sticky-cta__btn">Обсудить проект</a>
<span class="sticky-cta__hint">Средний отклик — менее 2 часов</span>
</div>
Зачем: Sticky CTA на мобильном повышает конверсию на 15-30% (источники: Baymard Institute, CXL). Пользователь всегда видит кнопку действия, не нужно скроллить.
3. 👆 Touch Targets (WCAG 2.5.5)
Проблема: кнопки мобильного меню и ссылки имеют произвольный размер. На телефоне с пальцем трудно попасть.
Исправление:
:root {
--touch-min: 44px; /* минимум по WCAG 2.5.5 и Apple HIG */
}
.mobile-menu-btn {
min-width: var(--touch-min);
min-height: var(--touch-min);
display: inline-flex;
align-items: center;
justify-content: center;
}
.mobile-menu a {
min-height: var(--touch-min);
transition: color 0.2s;
}
.mobile-menu a:active {
background: var(--accent-dim);
}
Зачем: Apple HIG требует 44×44pt, WCAG 2.5.5 — 44×44px. Меньше — палец не попадает, пользователь уходит.
4. 🎯 Touch Feedback
Проблема: на десктопе hover-эффекты, на мобильном — ничего. Пользователь не понимает, что нажал.
Исправление:
.btn {
/* Отключаем тёмный фон при тапе на Chrome/Android */
-webkit-tap-highlight-color: transparent;
will-change: transform;
}
/* Active state для мобильных */
.btn:active {
transform: scale(0.97); /* микро-сжатие как в нативных приложениях */
}
.sticky-cta__btn:active {
transform: scale(0.98);
}
Зачем: Визуальная обратная связь при тапе критична на тач-устройствах. Scale 0.97 создаёт ощущение «нажатия» без задержки.
5. 📦 Font Loading (FOIT/FOUT)
Проблема: Google Fonts загружается асинхронно, текст появляется сначала system font, потом Inter → видимый «скачок» шрифта.
Исправление:
<!-- Preload CSS-ресурса -->
<link rel="preload" href="https://fonts.googleapis.com/css2/..." as="style" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2/..." />
Зачем: preload сообщает браузеру приоритет загрузки шрифта. Без него шрифт может загрузиться через 2-3 секунды.
6. ♿ Accessibility
Предпочтение reduced motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.reveal { opacity: 1; transform: none; }
}
Зачем: ~15% пользователей имеют включённый reduced motion (вестибулярные нарушения, мигрень). Игнорирование prefers-reduced-motion — нарушение WCAG 2.2.1.
iOS Bounce (overscroll-behavior)
body {
overscroll-behavior-y: contain;
}
.mobile-menu {
overflow-y: auto;
overscroll-behavior-y: contain;
}
Зачем: На iOS Safari overscroll-behavior по умолчанию auto создаёт пружинящий эффект, который ломает ощущение «приложения». contain ограничивает bounce внутри элемента.
Backdrop-filter для всех браузеров
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px); /* Safari 9-15 */
Зачем: -webkit- префикс нужен для Safari на iOS 15 и младше. Без него blur не работает.
7. 📐 Layout on Mobile
Trust Bar 4-колонка → 2×2
@media (max-width: 768px) {
.trust-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
}
Hero-секция: 100dvh вместо 100vh
@media (max-width: 768px) {
#hero {
min-height: 100dvh;
}
}
Фото hero на мобильном — ограниченный размер
@media (max-width: 768px) {
.photo-hero-mobile {
max-height: 280px;
aspect-ratio: 4/5;
}
}
Зачем: Полноразмерное фото 600×900 на мобильном занимает весь экран. Ограничение до 280px оставляет место для CTA над фото.
Контейнер контактов: CTA вертикально
@media (max-width: 768px) {
.cta-stack {
flex-direction: column !important;
}
.cta-stack .btn {
width: 100%;
}
}
8. 🍔 Мобильное меню
Close button внутри
<button class="mobile-menu-close" aria-label="Закрыть меню" onclick="...">✕</button>
Emoji иконки для навигации
<a href="#services">📊 Услуги</a>
<a href="#products">📦 Продукты</a>
<a href="#cases">🏆 Кейсы</a>
<a href="#about">👤 Обо мне</a>
Зачем: На мобильном нет hover. Emoji даёт визуальный якорь. Close button в правом верхнем углу — стандартный паттерн iOS.
Сводная таблица всех изменений
| # | Категория | Изменение | Приоритет | Impact |
|---|---|---|---|---|
| 1 | Viewport | viewport-fit=cover |
🔴 Critical | iOS safe-area |
| 2 | Viewport | 100dvh для hero |
🔴 Critical | iOS контент |
| 3 | Layout | env(safe-area-inset-*) |
🔴 Critical | iPhone notch/Dynamic Island |
| 4 | CTA | Sticky bottom CTA | 🟡 High | +15-30% конверсия |
| 5 | Touch | 44×44px минимум | 🟡 High | WCAG 2.5.5 |
| 6 | Touch | :active feedback |
🟡 High | Пользовательский опыт |
| 7 | Touch | -webkit-tap-highlight-color |
🟡 High | UX на Android |
| 8 | Perf | Font preload | 🟡 High | Убирает FOIT |
| 9 | A11y | prefers-reduced-motion |
🟢 Medium | 15% пользователей |
| 10 | A11y | overscroll-behavior-y |
🟢 Medium | iOS bounce |
| 11 | A11y | -webkit-backdrop-filter |
🟢 Medium | Safari iOS |
| 12 | Layout | Hero photo limit | 🟢 Medium | Компоновка |
| 13 | Layout | Trust bar 2×2 | 🟢 Medium | Компактность |
| 14 | Layout | CTA vertical stack | 🟢 Medium | Touch-удобство |
| 15 | Layout | Container safe padding | 🔴 Critical | Контент не обрезан |
| 16 | Layout | main bottom padding |
🟡 High | Sticky CTA не перекрывает |
| 17 | Nav | Close button mobile | 🟡 High | Мобильное меню |
| 18 | Nav | Emoji icons | 🟢 Low | Мобильная навигация |
| 19 | Nav | padding-top safe-area |
🟡 High | Навигация не обрезана |
| 20 | Nav | overflow-y: auto menu |
🟢 Medium | Scroll внутри меню |
Антипаттерны, которых стоит избегать
❌ 100vh вместо 100dvh
/* БУДЬТЕ ОСТОРОЖНЫ — iOS Safari */
.hero { min-height: 100vh; } /* ← контент обрезан */
.hero { min-height: 100dvh; } /* ← правильно */
❌ Фиксированная позиция без safe-area
/* БУДЬТЕ ОСТОРОЖНЫ — iPhone X+ */
.footer { bottom: 0; } /* ← под home indicator */
.footer { bottom: env(safe-area-inset-bottom); } /* ← правильно */
❌ Touch-элементы меньше 44px
/* БУДЬТЕ ОСТОРОЖНЫ */
.icon-btn { width: 32px; height: 32px; } /* ← палец не попадёт */
.icon-btn { min-width: 44px; min-height: 44px; } /* ← правильно */
❌ Скрытие hover вместо адаптации
/* БУДЬТЕ ОСТОРОЖНЫ */
.btn:hover { background: gold; } /* ← на тач-экранах не работает */
.btn:active { background: gold; } /* ← добавьте для тач */
❌ Фиксированные размеры для шрифтов
/* БУДЬТЕ ОСТОРОЖНЫ */
h1 { font-size: 2.5rem; } /* ← одинаковый на всех экранах */
h1 { font-size: clamp(2rem, 6vw, 3.5rem); } /* ← адаптивный */
Чек-лист тестирования
Обязательные тесты:
- iPhone 15 Pro (Dynamic Island) — Safari
- iPhone SE (маленький экран) — Safari
- Android (Pixel) — Chrome
- iPad — Safari
- Landscape — Safari, Chrome
- Reduced motion — Settings > Accessibility
- Dark mode — системные настройки
Инструменты:
- Chrome DevTools — Device Toolbar (Ctrl+Shift+M)
- Safari Web Inspector — Remote debugging на iPhone
- Lighthouse — Performance, Accessibility, Mobile-friendly
- WebPageTest — реальные тесты с реальных устройств
- BrowserStack — реальные устройства, не только эмуляция
Метрики:
- LCP (Largest Contentful Paint) < 2.5s на мобильном
- INP (Interaction to Next Paint) < 200ms
- CLS (Cumulative Layout Shift) < 0.1
- Touch targets ≥ 44×44px
- Font loading < 1s (FOIT невидимый)
Итог
20 изменений, которые превратили десктопный лендинг в полноценное мобильное приложение:
- Safe area — контент не обрезан на iPhone с notch/Dynamic Island
- Sticky CTA — кнопка действия всегда видна → рост конверсии
- Touch targets — 44px минимум по стандартам Apple/Google
- Touch feedback —
:activescale вместо ненавидимого hover - dvh — hero занимает весь экран без обрезки
- Accessibility — reduced-motion, overscroll, WCAG
- Performance — preload шрифтов
Общий размер файла: +8KB (было 101KB, стало 109KB)