Все уроки
ФронтендСредний13 мин чтенияВ моём стеке

SEO, производительность и доступность

Core Web Vitals, техническое SEO и основы WCAG — измеряем, а не гадаем.

  • Core Web Vitals
  • JSON-LD
  • hreflang
  • Sitemaps
  • Accessibility
Шаг 1/5
Поисковый crawler заходит на страницу, которая — просто пустая JS-заготовка: <div id=root> и спиннер. Читать пока нечего.

За 30 секунд

Сайт должен находиться, быстро загружаться и работать для всех. Техническое SEO помогает поисковикам обходить и понимать ваши страницы; Core Web Vitals измеряют, насколько быстрыми и стабильными они ощущаются для реальных пользователей; доступность гарантирует, что ими смогут пользоваться люди с клавиатурой, скринридером или увеличением. Эти три области сильно пересекаются, и лучше всего с ними работать, измеряя реальные данные и исправляя сначала самую крупную проблему.

Почему я это использую

  • Мне это важно, потому что интернет-магазин, который плохо ранжируется или медленно грузится, теряет покупателей ещё до того, как они увидят хоть один товар.
  • Я встраиваю SEO в CMS с самого начала: метаданные для каждой страницы, sitemap, редиректы и структурированные данные, чтобы редакторам никогда не требовался разработчик ради исправления заголовка.
  • Я работаю с многоязычными сайтами, где hreflang и чистые URL с локалью решают, дойдёт ли каждая языковая версия до нужной аудитории.
  • Я считаю доступность частью качества, и она помогает SEO, потому что семантические, хорошо структурированные страницы проще понять и людям, и краулерам.

Как это работает

01Core Web Vitals: LCP, INP и CLS

Core Web Vitals — три метрики, которыми Google описывает опыт реальных пользователей. Largest Contentful Paint (LCP) — момент, когда появляется основной контент, обычно hero-изображение или заголовок; хорошо — 2.5 секунды или меньше. Interaction to Next Paint (INP) измеряет, насколько быстро страница визуально откликается на клики, тапы и нажатия клавиш за весь визит; хорошо — 200 миллисекунд или меньше. INP заменил First Input Delay в марте 2024 года. Cumulative Layout Shift (CLS) измеряет, насколько неожиданно прыгает контент; хорошо — 0.1 или меньше. Страница проходит проверку, когда 75% реальных визитов укладываются в каждый порог, причём мобильные и десктоп считаются отдельно. Это сигнал ранжирования, но скромный; гораздо важнее их влияние на конверсию и доверие пользователей.

  • LCP ≤ 2.5 s: загрузка.
  • INP ≤ 200 ms: отзывчивость.
  • CLS ≤ 0.1: визуальная стабильность.
  • Оценивается по 75-му перцентилю реальных пользователей.

02Как исправить каждую метрику

LCP состоит из четырёх частей: время ответа сервера (TTFB), задержка, прежде чем браузер обнаружит LCP-ресурс, время его загрузки и задержка рендеринга. Исправляйте в этом порядке: кэшируйте HTML на edge, делайте hero-изображение обнаружимым в HTML (а не заданным через JavaScript или CSS background), никогда не загружайте его лениво, ставьте ему fetchpriority="high" и отдавайте в AVIF или WebP нужного размера. INP — про работу главного потока: отправляйте меньше JavaScript, разбивайте длинные задачи, чтобы браузер успевал отрисовать кадр между ними (scheduler.yield() или setTimeout), используйте startTransition для несрочных обновлений React и избегайте тяжёлых сторонних скриптов. CLS — про зарезервированное место: всегда задавайте width и height или aspect-ratio изображениям и встраиваемым элементам, резервируйте место под баннеры и рекламу и используйте fallback-шрифты с совпадающими метриками.

html
<!-- LCP image: discoverable, eager, high priority, right size -->
<img
  src="/img/hero-1200.avif"
  srcset="/img/hero-800.avif 800w, /img/hero-1200.avif 1200w, /img/hero-1800.avif 1800w"
  sizes="(max-width: 768px) 100vw, 1200px"
  width="1200" height="675"
  fetchpriority="high"
  alt="Fresh coffee being poured into a cup" />
 
<!-- below-the-fold images -->
<img src="/img/team.avif" width="800" height="600" loading="lazy" decoding="async" alt="Our team" />

03Обход, индексация и canonical

Поисковые системы работают в три шага: обход (загрузка URL), индексация (сохранение и понимание) и ранжирование. robots.txt управляет обходом, а не индексацией: заблокированный URL всё равно может появиться в выдаче, если на него ссылаются другие сайты. Чтобы не пустить страницу в индекс, разрешите обход и отдайте мета-тег noindex или заголовок X-Robots-Tag. Ссылка canonical сообщает поисковикам, какой URL — основная версия, когда один и тот же контент доступен по нескольким адресам, например с параметрами отслеживания или фильтрами; это сильная подсказка, а не команда. Возвращайте настоящие коды статуса: 301 для постоянных переездов, 404 или 410 для удалённых страниц и никогда — страницу «не найдено» с кодом 200. Убедитесь, что важный контент есть в HTML, отрендеренном на сервере, а не только подгружается клиентским JavaScript.

text
# robots.txt
User-agent: *
Disallow: /cart
Disallow: /account
Disallow: /*?sort=
 
Sitemap: https://example.com/sitemap.xml

04Sitemap, структурированные данные и hreflang

Sitemap перечисляет URL, которые вы хотите проиндексировать, с точной датой lastmod; priority и changefreq Google игнорирует. Один файл вмещает до 50,000 URL или 50 MB без сжатия, поэтому крупные сайты используют sitemap index. Структурированные данные, обычно JSON-LD в теге script, описывают страницу словарём schema.org: Organization, Product с Offer, BreadcrumbList, Article, Event. Они могут открыть расширенные результаты, например цены и рейтинги. Для многоязычных сайтов hreflang сообщает поисковикам, какой URL для какого языка или региона. Каждая версия должна перечислять все версии, включая саму себя, ссылки должны быть взаимными, а x-default указывает на запасной вариант. Используйте коды языков вроде en, hy, ru, при необходимости с регионом, например en-GB.

html
<link rel="canonical" href="https://example.com/en/products/espresso" />
<link rel="alternate" hreflang="en" href="https://example.com/en/products/espresso" />
<link rel="alternate" hreflang="hy" href="https://example.com/hy/products/espresso" />
<link rel="alternate" hreflang="ru" href="https://example.com/ru/products/espresso" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/products/espresso" />
 
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "Product", "name": "Espresso beans 1 kg",
  "offers": { "@type": "Offer", "price": "24.90", "priceCurrency": "EUR",
              "availability": "https://schema.org/InStock" } }
</script>

05Оптимизация изображений и шрифтов

Изображения обычно самая тяжёлая часть страницы. Отдавайте современные форматы (AVIF, затем WebP), масштабируйте до отображаемого размера через srcset и sizes, сжимайте и лениво загружайте всё, что ниже первого экрана. Фреймворки помогают: компонент Image в Next.js автоматически генерирует размеры и форматы; в Next.js 16 старый проп priority заменён на preload, а документация рекомендует loading="eager" или fetchPriority="high" для LCP-изображения. Шрифты вызывают и медленное появление текста, и сдвиги раскладки. Хостите их сами, делайте subset под нужные символы (отдельные файлы для латиницы, кириллицы или армянского), делайте preload только одного-двух критичных файлов, используйте font-display: swap и fallback-шрифт с size-adjust, чтобы подмена не сдвигала текст. Вариативные шрифты могут заменить несколько файлов с разной насыщенностью одним.

css
@font-face {
  font-family: "Brand Sans";
  src: url("/fonts/brand-sans-latin.woff2") format("woff2");
  font-display: swap;
  unicode-range: U+0000-00FF;
}
 
/* metric-matched fallback reduces layout shift on swap */
@font-face {
  font-family: "Brand Sans Fallback";
  src: local("Arial");
  size-adjust: 104%;
  ascent-override: 92%;
}

06Основы доступности (WCAG)

WCAG (Web Content Accessibility Guidelines) — это стандарт; обычная цель — версия 2.2 уровня AA, и именно на неё ссылаются законы вроде European Accessibility Act, который применяется с июня 2025 года. Основы закрывают большинство реальных проблем. Используйте семантический HTML: один h1, заголовки по порядку, ориентиры (header, nav, main, footer), button для действий и a для ссылок. Каждому изображению нужен осмысленный alt-текст или alt="", если оно декоративное. Контраст текста — не меньше 4.5:1 или 3:1 для крупного текста и элементов интерфейса. Всё должно работать с клавиатуры, с видимым индикатором фокуса. Формам нужны настоящие label и понятные сообщения об ошибках. Области нажатия — не меньше 24 на 24 CSS-пикселя. Задавайте атрибут lang на каждой странице — для скринридеров на многоязычных сайтах это важно.

  • Сначала семантические элементы, ARIA — только когда в HTML нет аналога.
  • Клавиатура: порядок Tab, видимый фокус, никаких ловушек, Escape закрывает диалоги.
  • Контраст 4.5:1 для текста, 3:1 для крупного текста и элементов управления.
  • Тестируйте со скринридером (VoiceOver, NVDA) и при увеличении 200%.

07Измерения: Lighthouse, PageSpeed и CrUX

Данные бывают двух видов. Лабораторные данные получают при симулированной загрузке на фиксированном устройстве и сети: Lighthouse в Chrome DevTools или в CI. Они воспроизводимы и отлично подходят для отладки, но не могут напрямую измерить INP; его заменитель — Total Blocking Time. Полевые данные приходят от реальных пользователей Chrome: Chrome UX Report (CrUX) агрегирует последние 28 дней. PageSpeed Insights показывает оба вида для URL, а Search Console группирует полевые данные по всему сайту. Для собственных подробных цифр подключите библиотеку web-vitals и отправляйте метрики в аналитику, включая то, какой элемент был LCP и какое взаимодействие оказалось медленным. Оптимизируйте по полевым данным, проверяйте исправления в лаборатории, а затем ждите подтверждения в полевых данных.

ts
import { onLCP, onINP, onCLS } from "web-vitals/attribution";
 
function send(metric: { name: string; value: number; rating: string }) {
  navigator.sendBeacon("/api/vitals", JSON.stringify(metric));
}
 
onLCP(send);
onINP(send);
onCLS(send);

Когда применять

  • Каждый публичный сайт, с первого спринта, а не как исправление в неделю запуска.
  • E-commerce и контентные сайты, где выручку приносят органический поиск и конверсия.
  • Многоязычные сайты, где каждый язык должен ранжироваться на своём рынке.
  • Продукты, которые продаются в регионах с регулированием доступности, например в ЕС.

Когда не стоит

  • Внутренним инструментам за логином SEO не нужно, хотя производительность и доступность им всё равно нужны.
  • Не гонитесь за идеальным баллом Lighthouse в ущерб функциям, которые нужны пользователям; исправляйте то, что полевые данные показывают как медленное.

Альтернативы

  • Next.js Metadata API

    Генерирует заголовки, canonical, hreflang-альтернативы, sitemap и robots из кода в App Router.

  • WebPageTest

    Глубокое лабораторное тестирование с раскадровкой, waterfall-диаграммами и реальными устройствами во многих локациях.

  • Инструменты мониторинга реальных пользователей

    Vercel Speed Insights, Cloudflare Web Analytics или Sentry собирают полевые Web Vitals по каждому маршруту.

  • axe DevTools / проверки доступности Lighthouse

    Автоматические проверки доступности; они находят примерно треть проблем, так что ручное тестирование по-прежнему необходимо.

Лучшие практики

  1. Рендерите важный контент и метаданные на сервере.
  2. Делайте LCP-изображение обнаружимым, eager и с высоким приоритетом; остальное загружайте лениво.
  3. Резервируйте место под каждое изображение, встраиваемый элемент и баннер, чтобы не было сдвигов раскладки.
  4. Разбивайте длинные JavaScript-задачи и регулярно проверяйте сторонние скрипты.
  5. Один canonical URL на страницу, со взаимным hreflang для каждой локали.
  6. Генерируйте sitemap и структурированные данные из CMS, чтобы они никогда не устаревали.
  7. Мониторьте полевые Web Vitals и доступность в CI, а не раз в год.

Частые ошибки

  • Использовать robots.txt, чтобы скрыть страницу из выдачи; он только блокирует обход.
  • Лениво загружать hero-изображение, из-за чего LCP задерживается.
  • Невзаимные теги hreflang или указывающие на URL с редиректом — поисковики их игнорируют.
  • Рассчитывать на разметку FAQ или HowTo ради расширенных результатов; в 2023 году Google ограничил FAQ-результаты несколькими авторитетными сайтами и убрал HowTo-результаты.
  • Убирать обводку фокуса ради красоты, из-за чего сайтом невозможно пользоваться с клавиатуры.

Глубже

08Внутри INP: input delay, обработка и отрисовка

INP показывает примерно худшее взаимодействие за визит; на страницах с большим количеством взаимодействий игнорируется один выброс на каждые 50. У каждого взаимодействия три части, и у каждой своё лечение. Input delay — время до того, как ваш обработчик вообще начнёт работу, обычно потому что выполняется другая длинная задача: гидрация, tag manager или пакет аналитики. Processing time — это сами ваши обработчики событий. Presentation delay — работа после обработчиков до отрисовки следующего кадра: стили, layout и paint всего, что изменилось; она растёт с размером DOM и с большими перерендерами React. Long Animation Frames API (LoAF) сообщает, какие скрипты сделали кадр медленным, вплоть до URL исходника и функции, а attribution-сборка web-vitals прикрепляет эти записи к INP. Итак: дробите конкурирующие длинные задачи, чтобы сократить input delay; сначала рисуйте визуальный отклик, а остальное откладывайте, чтобы сократить обработку; держите DOM маленьким, а обновления локальными, чтобы сократить presentation delay.

ts
const yieldToMain = () =>
  "scheduler" in globalThis && "yield" in scheduler
    ? scheduler.yield()
    : new Promise((resolve) => setTimeout(resolve, 0));
 
button.addEventListener("click", async () => {
  showAddedState();      // 1. visual feedback in this frame
  await yieldToMain();   // 2. let the browser paint
  await saveCart();      // 3. heavy work afterwards
  sendAnalytics();
});

09Мгновенные переходы: bfcache и speculation rules

Самая быстрая загрузка страницы — та, которой не было. Back/forward cache (bfcache) хранит полный снимок страницы в памяти, поэтому «Назад» и «Вперёд» восстанавливают её мгновенно, без сети и без повторного выполнения JavaScript. Страница теряет это преимущество, если регистрирует обработчик unload, держит определённые открытые соединения в момент ухода пользователя или исключена заголовками вроде Cache-Control: no-store. В DevTools, в разделе Application и Back/forward cache, можно протестировать страницу и увидеть точный список причин блокировки. Для переходов вперёд Speculation Rules API позволяет браузерам на Chromium делать prefetch или полный prerender вероятных следующих страниц — по списку или по совпадению ссылок — с настройкой eagerness, например moderate (старт при наведении). Предварительно отрендеренная страница при открытии показывает почти нулевой LCP. Другие браузеры правила игнорируют, так что это безопасное прогрессивное улучшение. Делайте prerender только для GET-страниц без побочных эффектов, исключайте ссылки logout или add-to-cart и откладывайте аналитику до реального показа страницы через document.prerendering.

html
<script type="speculationrules">
{
  "prerender": [{
    "where": { "and": [
      { "href_matches": "/*" },
      { "not": { "href_matches": "/logout" } },
      { "not": { "href_matches": "/cart/*" } },
      { "not": { "selector_matches": "[data-no-prerender]" } }
    ]},
    "eagerness": "moderate"
  }]
}
</script>

10Краулинговый бюджет и рендеринг на больших сайтах

Сайт на несколько сотен страниц сканируется полностью. Когда URL сотни тысяч, как в фасетном каталоге, краулинговый бюджет становится реальным: Googlebot тратит ограниченное число запросов на сайт, и оно зависит от того, насколько быстр и здоров ваш сервер и насколько востребованы ваши URL. Потери создают бесконечные пространства URL: комбинации фильтров, порядки сортировки, параметры сессии или трекинга, бесконечные календари. Управляйте этим слоями. Заметно ссылайтесь только на те фасетные страницы, которые хотите проиндексировать. Остальные комбинации канонизируйте на базовую страницу или запрещайте параметры в robots.txt, если их не нужно даже запрашивать; помните, что заблокированный URL не сможет показать свой noindex. В sitemap держите только канонические индексируемые URL с честным lastmod. Логи сервера, отфильтрованные по верифицированному Googlebot, показывают, куда на самом деле уходит сканирование. Рендеринг тоже важен: Google выполняет JavaScript, но отдельным, более дорогим шагом, поэтому контент и ссылки, которые появляются только после клиентской загрузки, находятся позже и менее надёжно.

  • Быстрые стабильные ответы (низкий TTFB, мало ошибок 5xx) позволяют Googlebot сканировать больше.
  • Возвращайте 404 для пустых комбинаций фильтров вместо тонкой страницы с 200.
  • Отчёт Crawl stats в Search Console показывает коды ответов, типы файлов и статус хоста.

11Доступность за пределами чеклиста: имена, роли и живые обновления

Автоматические инструменты ловят только часть нарушений WCAG; для остального нужно понимать дерево доступности. Браузер превращает DOM в дерево объектов, у каждого есть роль (button, link, dialog), доступное имя (из текста, label, aria-label или aria-labelledby), состояния вроде expanded или checked и необязательное описание. Скринридеры читают это дерево, а не ваши пиксели; смотрите его в панели Accessibility в DevTools. Там проявляются более глубокие проблемы: иконочные кнопки без имени; самописные выпадающие списки, которыми нельзя управлять с клавиатуры; одностраничные переходы, которые меняют контент, не перемещая фокус, так что пользователи скринридеров не знают, что страница сменилась (переводите фокус на новый h1 или объявляйте заголовок); статусные сообщения вроде «добавлено в корзину», которые видны глазами, но никогда не озвучиваются. Для объявлений используйте live region, например role='status', который уже есть в DOM до того, как вы поменяете его текст. Нативный элемент dialog с showModal() сам удерживает фокус и обрабатывает Escape. Тестируйте ключевые сценарии с VoiceOver или NVDA.

html
<button type="button" aria-label="Open cart" aria-expanded="false" aria-controls="cart">
  <svg aria-hidden="true" focusable="false"><!-- icon --></svg>
</button>
 
<!-- present on page load; changing its text is announced politely -->
<p role="status" class="visually-hidden" id="cart-status"></p>
 
<dialog id="cart" aria-labelledby="cart-title">
  <h2 id="cart-title">Your cart</h2>
  <!-- opened with cart.showModal(): focus is trapped, Escape closes -->
</dialog>

Реальные ситуации

01

Органический трафик упал после миграции сайта

Проблема

Сайт на 5,000 страниц переехал на новый фреймворк с новыми URL. Через три недели органический трафик упал примерно на 40 процентов, а Search Console показывает много ошибок «Not found» и «Duplicate without user-selected canonical».

Как решить

Восстановите карту редиректов из всех источников старых URL: старых sitemap, посадочных страниц из аналитики и отчётов по обратным ссылкам. Каждый старый URL должен делать 301 на ближайший новый аналог за один шаг, без цепочек и без массовых редиректов на главную. Просканируйте список старых URL краулером, чтобы проверить коды ответов. Затем ищите остатки со staging: теги noindex, canonical, указывающие на staging-хосты, ограничительный robots.txt. Обновите hreflang и внутренние ссылки, чтобы они вели прямо на новые URL. Отправьте новый sitemap и через URL Inspection на ключевых страницах сравните отрендеренный HTML со старым сайтом. Восстановление обычно занимает недели, поэтому отслеживайте его по разделам.

02

Lighthouse показывает 95, а Search Console — Poor

Проблема

Лабораторные тесты дают отличные оценки, но Search Console сообщает о плохих LCP и INP для большинства мобильных URL. Команда не понимает, какой цифре верить.

Как решить

Доверяйте полевым данным, а затем объясните разрыв. Лабораторный тест загружает одну чистую страницу на симулированном устройстве; реальные пользователи приносят медленные телефоны, баннеры согласия, скрипты A/B-тестов, персонализацию для залогиненных и холодные кэши. Подключите attribution-сборку web-vitals и отправляйте в аналитику LCP-элемент, цель взаимодействия INP и ответственный скрипт. Часто LCP-элементом в поле оказывается cookie-баннер или другой hero для каждой страны, а TTFB медленный, потому что персонализированные страницы проходят мимо edge-кэша. Воспроизведите реальные условия в лаборатории с троттлингом, баннером согласия и включёнными сторонними тегами. Исправьте главную причину и дождитесь, пока 28-дневное окно CrUX обновится, прежде чем судить о результате.

03

Фасетные страницы категорий заполоняют индекс

Проблема

У интернет-магазина около 300 категорий, но Search Console показывает сотни тысяч обнаруженных URL, в основном комбинации фильтров и сортировок. Новые товары появляются в поиске неделями.

Как решить

Решите, у каких фасетов есть реальный поисковый спрос, например категория плюс бренд, и дайте им чистые статические URL, уникальные заголовки и тексты, внутренние ссылки и записи в sitemap. Для всех остальных комбинаций сохраните фильтры рабочими, но канонизируйте на базовую категорию, а чистые параметры сортировки, вида и размера страницы запретите в robots.txt, чтобы Googlebot перестал их запрашивать. Не совмещайте блок в robots.txt с noindex на одном URL, потому что noindex никогда не будет увиден. Возвращайте 404 для комбинаций без товаров. Опубликуйте отдельный sitemap товаров с точным lastmod, чтобы ускорить обнаружение, и следите за Crawl stats, пока доля URL с параметрами не упадёт.

Шпаргалка

Прогреть критичный сторонний origin
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
Уступить главный поток в долгой работе
await scheduler.yield(); // fallback: await new Promise(r => setTimeout(r, 0))
Следить за медленными кадрами и их скриптами
new PerformanceObserver((l) => console.log(l.getEntries())).observe({ type: "long-animation-frame", buffered: true });
Отложить работу до показа prerender-страницы
if (document.prerendering) document.addEventListener("prerenderingchange", init, { once: true }); else init();
Найти, что блокирует bfcache
DevTools > Application > Back/forward cache > Test back/forward cache
Убрать PDF или изображение из индекса
X-Robots-Tag: noindex (заголовок HTTP-ответа)
Запустить Lighthouse в CI
npx @lhci/cli autorun
Увидеть страницу так, как её отрендерил Google
Search Console > URL Inspection > View crawled page
Озвучить смену статуса для скринридеров
<p role="status">Added to cart</p>
Доступная модалка с удержанием фокуса
document.querySelector("dialog").showModal();

В моих реальных проектах

  • На платформе заказов для ресторанов и в магазине зоотоваров построенная мной CMS включает SEO-поля для каждой модели и генерируемый sitemap.
  • На сайте сети кофеен я настроил sitemap, robots, OG-изображения и файлы ассоциации app-link, которые позволяют ссылкам открываться в мобильном приложении.
  • У сайта digital-агентства, над которым я работал, есть генерация sitemap, robots и llms.txt, а также SEO-редиректы после полного переписывания.
  • На платформе бронирования туров на 8 языках мой вклад включал разметку SEO schema, редиректы и работу над производительностью.

Вопросы и ответы

Q1Что такое Core Web Vitals и каковы их пороги?

Их три. Largest Contentful Paint измеряет загрузку: когда рендерится самый крупный видимый элемент, обычно hero-изображение или главный заголовок; хорошо — 2.5 секунды или меньше. Interaction to Next Paint измеряет отзывчивость по всем взаимодействиям за визит; хорошо — 200 миллисекунд или меньше. В марте 2024 года он заменил First Input Delay. Cumulative Layout Shift измеряет неожиданные сдвиги контента; хорошо — 0.1 или меньше. Страница проходит, когда 75-й перцентиль реальных пользователей укладывается в каждый порог, отдельно для мобильных и десктопа. Данные берутся из Chrome UX Report за скользящее окно в 28 дней, поэтому улучшения становятся видны через несколько недель.

Q2У нас LCP 4 секунды. Как бы вы к этому подошли?

Сначала я нахожу LCP-элемент и раскладываю время на четыре части: time to first byte, задержка загрузки ресурса, длительность загрузки ресурса и задержка рендеринга. Это показывают PageSpeed Insights и панель Performance в DevTools. Если TTFB высокий, я кэширую HTML на CDN или использую статический или инкрементальный рендеринг. Если велика задержка, скорее всего изображение обнаруживается поздно — через JavaScript, CSS background или ленивую загрузку, — поэтому я помещаю его в HTML с fetchpriority high. Если медленная загрузка, отдаю AVIF или WebP правильного размера. Если высокая задержка рендеринга, ищу блокирующие рендер CSS, шрифты или гидрацию. Затем подтверждаю результат полевыми данными.

Q3Как улучшить INP в React-приложении?

INP плохой, когда в момент взаимодействия главный поток занят или когда обновление после него дорогое. Я профилирую медленное взаимодействие в DevTools и ищу длинные задачи. Исправления в порядке эффекта: отправлять меньше JavaScript через code splitting и серверные компоненты; убрать или отложить тяжёлые сторонние скрипты; держать немедленный отклик маленьким, например сначала обновить состояние кнопки, а дорогую работу обернуть в startTransition; разбивать длинные циклы через scheduler.yield(); виртуализировать длинные списки; и не перерендеривать огромные деревья, держа состояние локально. Attribution-сборка web-vitals показывает, какой элемент и какая фаза медленные в продакшене.

Q4Как настроить SEO для многоязычного сайта?

Каждый язык получает свой URL, обычно с префиксом пути вроде /en/ или /hy/, и никогда — только cookie или автоматический редирект по языку браузера, потому что краулеры такие сигналы не отправляют. Каждая страница содержит hreflang-альтернативы для всех языковых версий, включая саму себя, плюс x-default, и они должны быть взаимными и указывать на конечные индексируемые URL. У каждой версии свой самоссылающийся canonical; никогда не указывайте canonical переводов на английскую версию. Заголовки, описания, слаги и структурированные данные переводятся, атрибут lang у html совпадает, а sitemap перечисляет каждый локализованный URL. Предлагайте переключатель языка вместо принудительных редиректов, чтобы и пользователи, и краулеры могли добраться до каждой версии.

Q5В чём разница между robots.txt, noindex и canonical?

Они решают разные задачи. robots.txt управляет обходом: он просит ботов не загружать определённые пути, что экономит краулинговый бюджет на корзине, аккаунте или бесконечных комбинациях фильтров. Он не удаляет страницы из индекса, а если вы заблокировали страницу, бот даже не увидит на ней noindex. noindex, в виде мета-тега или заголовка X-Robots-Tag, управляет индексацией: страницу можно обойти, но в выдаче она не появится. Canonical объединяет дубликаты: он говорит поисковикам, какой URL должен представлять контент, доступный по нескольким адресам, и передаёт этому URL сигналы. Это сильная подсказка, а не гарантия.

Q6Какие основы доступности должен соблюдать каждый сайт?

Я начинаю с проблем, которые мешают большинству людей. Семантический HTML: настоящие кнопки и ссылки, заголовки по порядку, ориентиры и один main. Доступ с клавиатуры: каждый элемент управления достижим через Tab, видимый индикатор фокуса, никаких ловушек клавиатуры, диалоги переводят фокус внутрь и возвращают его. Текстовые альтернативы: осмысленный alt для информативных изображений и пустой alt для декоративных. Контраст не меньше 4.5:1 для обычного текста. Поля форм с подписями и ошибками, объяснёнными текстом, а не только цветом. Правильные атрибуты lang, учёт reduced motion и раскладки, которые работают при увеличении 200%. Затем я запускаю axe или Lighthouse и тестирую ключевые сценарии со скринридером, потому что автоматические инструменты пропускают большинство проблем.

Вопросы уровня senior

S1Как выбор рендеринга (SSG, ISR, SSR, клиентский рендеринг) влияет на SEO и Web Vitals?

Статическая генерация и инкрементальная регенерация дают лучшую базу: HTML собран заранее и закэширован на edge, поэтому TTFB низкий, LCP начинается рано, а краулеры получают полный контент. Компромисс — свежесть, которую решает ревалидация. Серверный рендеринг на каждый запрос подходит для персонализированных или быстро меняющихся страниц, но TTFB теперь зависит от самого медленного запроса данных, поэтому я сначала стримлю оболочку и агрессивно кэширую данные. Чистый клиентский рендеринг — самый слабый вариант для SEO и LCP: контент появляется только после того, как JavaScript скачается, выполнится и загрузит данные, а поисковики рендерят его отложенным шагом. Каждый серверный вариант ещё и стоит гидрации, которая бьёт по INP, если она большая, поэтому я держу интерактивные острова маленькими с помощью серверных компонентов. На практике я смешиваю подходы по роутам: статика для маркетинговых страниц и каталога, серверный рендеринг для поиска и личного кабинета, только клиент для приватных дашбордов.

S2Как не дать большой команде постепенно ухудшать производительность?

Сделав производительность видимой и закреплённой за людьми, а не разовым аудитом. Я задаю бюджеты по типам страниц: полевые LCP, INP и CLS на 75-м перцентиле плюс вес JavaScript и изображений. В CI Lighthouse CI запускается на preview-деплоях с assertions, а проверка размера бандла комментирует каждый pull request с дельтой. В продакшене мониторинг реальных пользователей через библиотеку web-vitals питает дашборд с разбивкой по типу страницы, устройству и стране, с алертами, когда 75-й перцентиль пересекает порог. Сторонние скрипты перед добавлением проходят ревью и получают владельца, потому что теги — самая частая тихая регрессия. Компромисс — немного трения для фичевых команд; я держу его маленьким: сначала бюджеты работают как предупреждения, а блокируют только явные измеримые регрессии.

S3Как вы обращаетесь со сторонними скриптами вроде tag manager, чат-виджетов и A/B-тестов?

Я отношусь к каждому как к затрате с владельцем. Сначала инвентаризация: что загружается, откуда, сколько килобайт и сколько времени главного потока, через панель Performance и network waterfall. Всё, у чего нет текущего владельца, удаляю. Для остального выбираю стратегию загрузки. Аналитика и tag manager грузятся после того, как страница стала интерактивной. Чат-виджеты грузятся по намерению: лёгкая кнопка-заглушка загружает настоящий виджет по клику. Клиентские A/B-тесты — самое сложное, потому что anti-flicker сниппеты скрывают страницу и уничтожают LCP, поэтому я предпочитаю эксперименты на сервере или на edge. Управление согласием должно быть быстрым, потому что оно часто становится LCP-элементом. Некоторые команды переносят скрипты в web worker инструментом вроде Partytown; это может помочь, но совместимость разная, поэтому я тестирую каждый тег.

Глоссарий

Input delay
Время между взаимодействием пользователя и моментом, когда начинает работать его обработчик события.
Presentation delay
Время от завершения обработчиков событий до отрисовки браузером следующего кадра.
Long Animation Frame (LoAF)
API браузера, сообщающий о кадрах дольше 50 мс вместе со скриптами, которые их вызвали.
bfcache
Back/forward cache: снимок в памяти, который делает переходы «Назад» и «Вперёд» мгновенными.
Speculation rules
JSON-скрипт, позволяющий браузерам на Chromium делать prefetch или prerender страниц, которые пользователь, вероятно, откроет.
Краулинговый бюджет
Сколько URL поисковик готов и способен запросить с сайта за определённое время.
Фасетная навигация
Просмотр через фильтры (размер, цвет, бренд), который может порождать огромное число комбинаций URL.
Дерево доступности
Модель страницы в браузере для вспомогательных технологий: роли, имена, состояния и связи.
Доступное имя
Текст, который скринридер озвучивает для элемента; вычисляется из содержимого, label или ARIA.
Live region
Элемент, изменения текста которого озвучиваются автоматически, например role='status' или aria-live.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.