Інженерія

Ми думали, що сайт швидкий. PageSpeed показав 55 — і проблема була не там, де всі шукають

Ми були впевнені, що сайт швидкий. Сервер віддає сторінку за 0,4 секунди, кеш працює, картинки не по 4 мегабайти, тема писана з нуля без жодного конструктора. Потім ми відкрили PageSpeed Insights, перемкнули на «Мобільні пристрої» — і побачили 58.

PageSpeed Insights до оптимізації: 58 балів ефективності, TBT 1160 мс
Так виглядав звіт до змін. Заміряно на дев-копії зі старим кодом, щоб не чекати машини часу (тому 66 у SEO — на дев-домені стоїть noindex)

Найцікавіше почалось у списку проблем. Там не було нічого з того, на що зазвичай думають: ні повільного сервера, ні гігантських JPEG, ні «занадто багато плагінів». Була одна рядок-фраза, яка пояснювала все:

Діагностика PageSpeed: мінімізуйте роботу основного потоку 9,4 с, 13 тривалих завдань
9,4 секунди роботи головного потоку і 13 тривалих завдань — на сторінці, де немає жодного важкого віджета

За один робочий день ми довели мобільні показники до 97–100 / 100 / 100 / 100 і заразом отримали 3/3 у новому блоці «Агентний вебперегляд». Нижче — реальний код, який це зробив, що з оптимізацій не спрацювало, і чому останній блок скоро стане важливішим за решту.

Спершу — чесний дисклеймер

  • Це лабораторні заміри. Lighthouse ганяє сторінку на емульованому Moto G Power з обмеженим 4G. Реальні користувачі на швидкому каналі побачать краще, на дешевому телефоні в метро — гірше.
  • Показник плаває. Той самий сайт у трьох прогонах поспіль дає 97, 99 і 100. Ми беремо медіану трьох — і радимо так само робити всім, хто збирається сперечатись про «а в мене 89».
  • 100 — не самоціль. Ціль — щоб сторінка відкривалась і починала слухатись пальця за секунду. Бал — лише зручний спосіб перевірити, що ти цього досяг.

Де ховались 40 балів

1. Нескінченна анімація рядка логотипів — 950 мс роботи процесора

Стрічка з логотипами клієнтів їхала через requestAnimationFrame: кожен кадр JavaScript рахував зсув і писав його в transform. Виглядало ідеально, коштувало — майже секунду роботи головного потоку в перші секунди завантаження. Ось як це було:

// було: JS рахує позицію кожного кадру
const tick = (now) => {
  const dt = (now - last) / 1000;
  last = now;
  state.forEach(s => {
    s.offset += s.dir * s.speed * dt;
    if (s.offset <= -s.half) s.offset += s.half;
    s.track.style.transform = `translate3d(${s.offset.toFixed(2)}px,0,0)`;
  });
  rafId = requestAnimationFrame(tick);
};

А ось те саме, але на композиторі — головний потік узагалі не бере участі. Треки й так дублюються, тож зсув на -50% зациклюється безшовно:

/* стало: анімація живе в CSS */
@keyframes cc-marq-l { from { transform: translate3d(0,0,0) }
                       to   { transform: translate3d(-50%,0,0) } }

.lm-track { animation: cc-marq-l var(--lm-dur, 70s) linear infinite }
.stack-row[data-dir="right"] .stack-track { animation: cc-marq-r 75s linear infinite }

@media (prefers-reduced-motion: reduce) { .lm-track, .stack-track { animation: none } }

JS лишився рівно один — раз поміряти ширину треку й перерахувати сталу швидкість (45 px/с) у секунди анімації.

2. three.js заради одного прямокутника

Анімований градієнт у шапці — це фрагментний шейдер на повний екран. Він тягнув three.js: 1,3 МБ бібліотеки (252 КБ стисненими) заради того, щоб намалювати два трикутники й передати три змінні у відеокарту. Ми викинули бібліотеку й написали те саме на «голому» WebGL:

const gl = canvas.getContext('webgl', { antialias:false, alpha:false, depth:false });

const compile = (type, src) => {
  const sh = gl.createShader(type);
  gl.shaderSource(sh, src); gl.compileShader(sh);
  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh));
  return sh;
};

const program = gl.createProgram();
gl.attachShader(program, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAG));   // той самий шейдер, що й був
gl.linkProgram(program);
gl.useProgram(program);

// один трикутник, що накриває весь екран
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);

Разом із циклом анімації, ресайзом і паузою за екраном — близько 3 КБ коду замість 1,3 МБ. Візуально градієнт не змінився жодним пікселем.

3. Карта клієнтів, яку ніхто ще не бачив

Секція з картою світу підвантажувала d3-geo, topojson і файл геометрії країн на 740 КБ — одразу на старті, хоча сама карта лежить у самому низу сторінки. Тепер усе це приїжджає, коли до секції лишається 400 px:

const section = document.querySelector('.clients-map');

const io = new IntersectionObserver((entries) => {
  if (entries.some(e => e.isIntersecting)) {
    io.disconnect();
    boot();                       // тільки тут вантажимо d3 + topojson + geojson
  }
}, { rootMargin: '400px 0px' });

io.observe(section);

4. Google Fonts як третя сторона

Шрифт Manrope приїжджав із fonts.googleapis.com, а сам файл — із fonts.gstatic.com: два зайві DNS-запити й два TLS-рукостискання на критичному шляху. Забрали до себе, дві підмножини замість шести:

/* assets/css/fonts.css — свій, а не Google */
@font-face{
  font-family:'Manrope'; font-style:normal; font-weight:400 800; font-display:swap;
  src:url('../fonts/manrope-cyrillic.woff2') format('woff2');
  unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

5. WooCommerce на сторінках, де немає магазину

WooCommerce підключає свої стилі й скрипти скрізь — на головній, у блозі, на сторінці «Про нас». Знімаємо там, де немає магазину:

function catcode_needs_woo_assets(): bool {
    if (!function_exists('is_woocommerce')) return false;
    if (is_woocommerce() || is_cart() || is_checkout() || is_account_page()) return true;
    if (is_singular(['module', 'product'])) return true;
    if (function_exists('WC') && WC()->cart && !WC()->cart->is_empty()) return true;
    // сторінка з shortcode/блоком магазину — теж лишаємо
    return false;
}

6. Картинки — так, теж

Картки кейсів віддавались повнорозмірними JPEG на 1440 px навіть на телефон. Тепер кожен JPEG і PNG у медіатеці має WebP-двійника, а тема сама загортає розмітку в <picture> з фолбеком на оригінал:

add_filter('wp_get_attachment_image', function ($html) {
    $srcset = catcode_webp_srcset($html);   // '' якщо хоч для одного розміру немає .webp
    if ($srcset === '') return $html;

    return sprintf('<picture><source type="image/webp" srcset="%s"%s>%s</picture>',
        esc_attr($srcset), $sizes, $html);
});

Що вийшло

PageSpeed Insights після оптимізації: 97 ефективність, 100 доступність, 100 оптимальні методи, 100 SEO, 3/3 агентний вебперегляд
Той самий сайт після змін: FCP 1,1 с, LCP 1,5 с, TBT 100 мс, CLS 0
Діагностика PageSpeed після оптимізації: два тривалих завдання замість тринадцяти
Було 13 тривалих завдань і 9,4 с роботи головного потоку — лишилось 2 завдання

Що НЕ допомогло

Половина порад із типових чеклістів у нашому випадку не дала нічого — і це не менш корисно знати, ніж те, що спрацювало.

  • WebP сам по собі. Картинки на головній лежать нижче першого екрана й вантажаться ліниво. Вони роздули вагу сторінки, але на бал майже не впливали — приріст пішов від JavaScript, а не від них.
  • defer на всіх скриптах. Він у нас уже стояв — усі скрипти теми були відкладені до оптимізації. Це не рятує, якщо відкладений скрипт потім малює кожен кадр.
  • Preload «про всяк випадок». Preload усього підряд просто переставляє чергу запитів. Реально допомогли рівно два: шрифт і логотип у шапці.
  • Зменшення роздільності шейдера. Ми чесно спробували лишити живу WebGL-анімацію на мобільному, знизивши рендер до 0,4× і 30 fps, потім до 0,25× і 20 fps. TBT впав із 594 до 273 мс і далі не рухався: у Lighthouse мобільний рендер іде без відеокарти, програмно. Компроміс — на телефоні спершу показується стоп-кадр цього ж шейдера (17 КБ WebP), а анімація стартує після першого дотику чи скролу.
  • Дрібні мікрооптимізації. Зняття WooCommerce-асетів, прибирання зайвого CSS з головної, відкладений gtag — усе разом дало приблизно 2–3 бали. Значення вони мають лише тоді, коли великі проблеми вже закриті.

Агентний вебперегляд — новий блок, на який варто дивитись

У травні 2026 Google перевів категорію Agentic Browsing («Агентний вебперегляд») з експериментальної в основну конфігурацію Lighthouse, і невдовзі вона зʼявилась у PageSpeed Insights. Оцінка тут не 0–100, а співвідношення пройдених перевірок: 3/3, 2/3 і так далі.

Питання, на яке відповідає цей блок: чи може AI-агент прочитати сторінку, зрозуміти її елементи керування й виконати задачу, не вгадуючи? Ось як виглядають ті три перевірки в реальному звіті:

Блок агентного вебперегляду в PageSpeed Insights: дерево доступності, CLS 0, файл llms.txt
Дерево доступності, Cumulative Layout Shift і llms.txt — три перевірки, які проходить звичайний сайт
  • Дерево доступності. Чи мають інтерактивні елементи програмні назви, коректні ролі й правильну вкладеність. Те саме, що потрібно скрінрідеру — тепер потрібно й агенту.
  • Стабільність макета (CLS). Щоб агент не клікнув туди, де кнопка була пів секунди тому.
  • Файл llms.txt у корені домену — машиночитний markdown-опис сайту: що це за проєкт, які ключові сторінки, де документація.

Є ще четверта перевірка — WebMCP, розмітка форм і дій сайту так, щоб агент міг ними користуватись напряму. Вона зʼявляється лише в тих, хто цей експериментальний протокол уже впровадив, тому в звичайного сайту максимум — 3/3.

Наші 3/3 — не випадковість: llms.txt ми виклали ще навесні, разом зі структурованими даними для сторінок модулів, а доступність тримаємо на 100 з першого дня. Що важливо розуміти: це не фактор ранжування, і категорія прямо позначена як така, що ще розвивається. Але напрямок читається однозначно — якість сайту офіційно перестала бути питанням лише людського досвіду. Половина ваших майбутніх «відвідувачів» — це асистенти, які приходять по відповідь замість користувача.

Головний висновок

Найбільший приріст нам дали не дрібні оптимізації, а відмова від важких JS-анімацій у першому екрані. Один рядок логотипів на requestAnimationFrame і одна бібліотека на 1,3 МБ коштували більше балів, ніж усі картинки, шрифти й сторонні скрипти разом узяті.

Практичний висновок для будь-якого сайту: перш ніж стискати картинки, відкрийте вкладку Performance і подивіться, хто малює кожен кадр. Найдорожче на сторінці — не те, що важить, а те, що працює безперервно.

Що з цього забрати собі

  • Не вірте одному прогону PageSpeed — беріть медіану трьох, інакше сперечаєтесь із випадковим числом.
  • Шукайте requestAnimationFrame і бібліотеки, підключені заради однієї функції.
  • Усе, що нижче першого екрана, має вантажитись при наближенні до нього, а не на старті.
  • Шрифти й скрипти з чужих доменів — це рукостискання, за які платить перший показ тексту.
  • Перевірте свій llms.txt і дерево доступності зараз, поки блок «Агентний вебперегляд» ще ніхто не читає уважно.

Якщо у вас на мобільному менше 70 і незрозуміло, з чого починати — напишіть нам. Подивимось ваш сайт і скажемо, які три речі дадуть найбільший приріст саме у вашому випадку, без загальних порад про «стисніть картинки».

Поділитись

Готові обговорити реліз?

Відповідаємо протягом 60 хвилин у робочий час — з оцінкою бюджету, орієнтовним графіком і складом команди під вашу задачу.

Залишити запит