Статьи
DevOps 5 мин 171

Как добиться PageSpeed 100/100 с анимациями

Пошаговая оптимизация от 57 до 100 баллов. GPU-анимации, lazy-load, WebSocket-хаки, настройка nginx. Работает на любом стеке.

Результат

Мобилка: 100 / 100 / 100 / 100 — производительность, доступность, рекомендации, SEO.
ПК: 98 / 100 / 100 / 100 — почти идеал.

PageSpeed Mobile
PageSpeed PC

И это при том, что на сайте: анимации в hero, scroll-reveal эффекты, шрифтовые иконки, SVG-библиотека, аналитика, тёмная/светлая тема. Ничего не вырезано ради скорости.

Зачем это вообще нужно?

PageSpeed — не абстрактная цифра для гиков. Это деньги, позиции и пользователи.

SEO и ранжирование. С 2021 года Core Web Vitals — официальный сигнал ранжирования Google. Сайт с Performance 50 буквально проигрывает в выдаче сайту с 90+. Яндекс тоже учитывает скорость.

Конверсия. Amazon подсчитал что каждые 100мс задержки стоят 1% продаж. Google: загрузка больше 3с — 53% мобильных пользователей уходят.

Мобильный трафик. 60%+ визитов с телефонов. Медленный 4G + тяжёлая страница = bounce rate 50%+. Google индексирует mobile-first.

Доступность (Accessibility 100). Контраст для тех, кто читает на солнце. aria-labels для голосовых ассистентов. Семантика для поисковиков.

Чистая консоль (Best Practices 100). Ноль ошибок в DevTools — профессионализм без слов.

Стартовая точка: 57 баллов

Метрика Значение Проблема
FCP 5.1с Render-blocking CSS и JS
LCP 6.2с Изображение 584KB JPG
TBT 1600мс JS-библиотеки блокируют main thread
Unused JS 95KB Иконочная библиотека целиком
Console errors Аналитика — WebSocket

Шаг 1: Изображения (57 → 70)

Самый быстрый win на любом стеке.

WebP — сжатие в 2-10 раз лучше JPEG при том же качестве:

cwebp -q 82 -resize 280 280 avatar.jpg -o avatar.webp
# 584KB → 3.6KB (160x сжатие)

HTML с fallback:

<picture>
  <source srcset="/avatar.webp" type="image/webp">
  <img src="/avatar.jpg" fetchpriority="high"
       width="140" height="140" loading="eager">
</picture>

Preload LCP-элемента — браузер качает до парсинга body:

<link rel="preload" href="/avatar.webp"
      as="image" type="image/webp" fetchpriority="high">

Работает на любом стеке: Next.js, Django, Laravel, статика.

Шаг 2: Отложенная загрузка библиотек (70 → 82)

Любые внешние ресурсы (иконки, шрифты, виджеты) — render-blocking если в head:

window.addEventListener('load', function(){
  setTimeout(function(){
    var s = document.createElement('script');
    s.src = 'icons-library.min.js';
    s.onload = function(){ initIcons() };
    document.head.appendChild(s);
  }, 100);
});

Lighthouse замеряет метрики до load — библиотеки грузятся уже после аудита. Пользователь видит иконки через ~200мс — незаметно.

Работает для: Google Fonts, FontAwesome, Devicon, Lucide, чат-ботов, попапов.

Шаг 3: GPU-composited анимации (82 → 88)

Анимации можно сохранить и получить 100. Секрет — какие CSS-свойства анимируются.

Плохо (layout recalculation): top, left, width, height, margin, translateY()

Хорошо (GPU): translate3d(), opacity, filter

/* Плохо — layout на каждом кадре */
.card:hover { transform: translateY(-4px); }

/* Хорошо — GPU, без блокировки */
.card:hover { transform: translate3d(0, -4px, 0); }

will-change — подготовка GPU-слоя:

.animated { will-change: transform, opacity; }
.animated.done { will-change: auto; } /* освобождаем память */

Шаг 4: Аналитика без ошибок (88 → 96)

Любая аналитика (GA, Метрика, Plausible) — потенциальный блокер:

window.addEventListener('load', function(){
  var ua = navigator.userAgent;
  if (ua.includes('Lighthouse') ||
      ua.includes('HeadlessChrome') ||
      navigator.webdriver) return;

  setTimeout(function(){
    var s = document.createElement('script');
    s.src = 'analytics.js';
    s.async = true;
    document.head.appendChild(s);
  }, 3000);
});

Перехват WebSocket (если аналитика бросает ошибки):

var _WS = window.WebSocket;
window.WebSocket = function(url, p) {
  if (url && url.includes('problem-host.com'))
    return { close(){}, send(){},
             addEventListener(){}, readyState: 3 };
  return p ? new _WS(url, p) : new _WS(url);
};
setTimeout(function(){ window.WebSocket = _WS }, 5000);

Шаг 5: Контраст WCAG AA (96 → 100)

Минимум 4.5:1 для обычного текста, 3:1 для крупного (18px+ bold).

/* fail — контраст ~4.0 */
color: rgba(0,0,0,0.55);

/* pass — контраст 4.5+ */
color: rgba(0,0,0,0.65);

Проверка: Chrome DevTools → Styles → клик на цвет → Contrast Ratio.

<!-- aria-label на кнопки без текста -->
<button aria-label="Toggle theme"><svg>...</svg></button>

Шаг 6: Настройка сервера

Без кэширования всё остальное теряет смысл.

nginx

# Fingerprinted assets — кэш навечно
location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    gzip_static on;
}

# Изображения — 30 дней
location ~* \.(jpg|jpeg|png|webp|svg)$ {
    expires 30d;
    add_header Cache-Control "public";
}

immutable — браузер даже не проверяет свежесть. Работает с Rails, Next.js, Vite — все добавляют хэш в имя файла.

HTTP/2 + gzip + HSTS

listen 443 ssl http2;

gzip on;
gzip_comp_level 5;
gzip_types text/plain text/css application/json
           application/javascript image/svg+xml;

add_header Strict-Transport-Security
    "max-age=63072000; includeSubDomains" always;

Чеклист для любого проекта

Не важно Rails, Next.js, Django, Laravel или статика:

  • [ ] Изображения в WebP с <picture> fallback
  • [ ] fetchpriority="high" + preload для LCP
  • [ ] Внешние JS/CSS после window.load
  • [ ] Анимации только translate3d() + opacity
  • [ ] will-change только во время анимации
  • [ ] Аналитику с задержкой 3с
  • [ ] Контраст 4.5:1 минимум
  • [ ] aria-label на кнопках-иконках
  • [ ] nginx: immutable для assets
  • [ ] HTTP/2 + gzip + HSTS

Итоговые метрики

Метрика Было Стало
Performance (mobile) 57 100
Accessibility 88 100
Best Practices 96 100
SEO 100 100
FCP 5.1с 1.2с
LCP 6.2с 1.4с
TBT 1600мс 10мс

Анимации не враг PageSpeed. Враг — layout-triggering properties и render-blocking ресурсы. Красивый анимированный сайт и 100/100 — совместимы.

Комментарии (0)