Mobile PSI 44 → 100 на legacy MODX: пошаговый гайд

Веб Штурм
  • PSI
  • Core Web Vitals
  • MODX
  • оптимизация
  • lazy-load

PSI Mobile 44 — это потеря половины мобильного трафика на этапе загрузки. На сайте ozsm.ru (legacy MODX с 2018 года, B2B вибропогружатели) подняли PSI Mobile с 44 до 100, Accessibility с 84 до 100, Best Practices до 96, SEO до 100. Все Core Web Vitals в зелёной зоне. Без переписывания сайта, на старом MODX, за десять дней. Ниже — реальные цифры по каждому фиксу.

Контекст

Стартовое состояние ozsm.ru на 23 апреля 2026:

Состояние через 10 дней:

МетрикаБылоСтало
PSI Mobile Performance44100
PSI Desktop Performance68100
PSI Accessibility84100
FCP Mobile8,8 с1,1 с
LCP Mobile11,3 с1,9 с
TBT1 108 мс0 мс
CLS0,0010,000
jQuery bootup2,4 с0,6 с (-75%)
Bundle JS~520 КБ~50 КБ (gzip)

Каждый патч — отдельный коммит с замером до/после. Ниже разбираем критические.

B11.3: lazy Я.Карты через IntersectionObserver

Виджет Яндекс.Карт = ~250 КБ JS + 200 КБ CSS на странице, где карта показывается. Если она ниже первого экрана — её можно вообще не грузить, пока юзер не подскроллит.

<!-- На странице — placeholder без подключения api-maps.yandex.ru -->
<div id="ymap-placeholder"
     style="height: 400px; background: #1a1a1a;"
     data-coords="53.2415,50.2212"
     data-zoom="14"></div>
const mapEl = document.getElementById('ymap-placeholder');
if (mapEl) {
  const observer = new IntersectionObserver((entries) => {
    if (!entries[0].isIntersecting) return;
    observer.disconnect();

    const s = document.createElement('script');
    s.src = 'https://api-maps.yandex.ru/2.1/?lang=ru_RU&apikey=YOUR_KEY';
    s.onload = () => ymaps.ready(initMap);
    document.head.appendChild(s);
  }, { rootMargin: '200px' });
  observer.observe(mapEl);
}

rootMargin: '200px' — карта начинает загружаться за экран до того как появится в viewport, так что пользователь не увидит «пустого места».

Эффект: 250 КБ JS откладываются в 30 строк vanilla.

B11.3c: Я.Метрика lazy через requestIdleCallback

Стандартный Я.Метрика-снippet грузит mc.yandex.ru/metrika/tag.js сразу при загрузке страницы. Это +120 КБ JS до первого взаимодействия.

Lazy-инициализация обёртывает init в requestIdleCallback({timeout:3000}) плюс триггеры на первое взаимодействие плюс 7-секундный fallback-таймер. Защита от Lighthouse simulation тоже здесь — Lighthouse не двигает мышью и не скроллит, поэтому без 7-секундного fallback Метрика просто не успевает инициализироваться в замере.

// /public/metrika-init.js
(function() {
  const COUNTER_ID = window.__YA_COUNTER_ID__;
  if (!COUNTER_ID) return;

  let initted = false;
  function init() {
    if (initted) return;
    initted = true;
    (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
    m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],
    k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
    (window,document,"script","https://mc.yandex.ru/metrika/tag.js","ym");

    window.ym(COUNTER_ID, "init", {
      clickmap: false,        // -50 КБ
      webvisor: false,        // -200 КБ
      accurateTrackBounce: true,
      trackHash: true,
      trackLinks: true,
    });
  }

  function schedule() {
    if (window.requestIdleCallback) {
      window.requestIdleCallback(init, { timeout: 3000 });
    } else {
      setTimeout(init, 1000);
    }
  }

  ['scroll', 'click', 'touchstart', 'keydown'].forEach(e => {
    document.addEventListener(e, schedule, { once: true, passive: true });
  });
  setTimeout(schedule, 7000);
})();

Эффект: TBT минус ~300 мс, bundle JS минус ~250 КБ.

Critical CSS inline без preload+onload

Очень популярный совет — «загрузите CSS асинхронно через <link rel=preload> + onload=this.rel='stylesheet'». На большом legacy-сайте без полного critical CSS этот паттерн даёт CLS 0,443 (Poor zone) — догружающийся bundle меняет шрифты, отступы, размеры.

Лучше: ~1.5 КБ Critical CSS прямо в <head>, render-blocking, и потом основной bundle уже не render-blocking. Размер бандла перестаёт играть роль для FCP/LCP.

На MODX это сделали через chunk: один chunk с минифицированным CSS только для above-the-fold (header, hero, основная типографика). Размер — 1,3 КБ gzip.

Дедупликация JS и удаление мёртвого кода

После аудита bundle нашли:

Подключили minifyJs=1 в MODX-конфигурации — оставшийся JS сжат и минифицирован.

Эффект: jQuery bootup 2,4 с → 0,6 с (-75%).

WebP + lazy на каталоге и продуктах

MODX-плагин на лету оборачивает все <img> в <picture> с <source srcset="...webp"> плюс loading="lazy" для всех картинок ниже первого экрана.

Размер каталога с 200+ карточек — 6 МБ JPG → 1,8 МБ WebP. Lazy-загрузка не грузит то, до чего юзер не доскроллил. На каталоге PSI Mobile 49 → 99.

A11y фиксы для +16 пунктов

Пять конкретных правок, которые подняли Accessibility с 84 до 100:

  1. <meta name="viewport"> — убрали user-scalable=no (запрет зума — нарушение WCAG)
  2. <main> landmark — обернули контент в <main>, а не в <div>
  3. aria-hidden="true" на декоративном swiper — слайдер с фотографиями не должен читаться скрин-ридером
  4. Color-contrast WCAG AA — поправили цвета кнопок «cookie consent» (было 3,2:1, стало 4,8:1)
  5. Target-size WCAG 2.5.5 — все интерактивные элементы 44×44 минимум. Где визуально хочется меньше — border: 12px solid transparent вокруг 20×20 кнопки (хитрейка: hit-area 44×44, визуально 20×20)

Что мы из этого вынесли

  1. PSI 100 на legacy достижимо за 10 дней. Не нужно переписывать сайт. Lazy-load + critical CSS + дедуп JS + WebP — это 80% подъёма.
  2. Lazy Я.Метрика — лучший компромисс. Аналитика остаётся, влияние на Core Web Vitals — близко к нулю.
  3. A11y даёт «бесплатные» очки. Пять правок, день работы, +16 пунктов.

Ссылки