Отиди до основното съдържание

Ръководство за оптимизация на производителността

Задълбочен преглед на техниките за оптимизация на уеб производителността, от Core Web Vitals до разширени стратегии за кеширане.

James Mitchell James Mitchell
12 мин. четене
Илюстрация: каскадна диаграма на зареждането на страница с отбелязано изобразяване на най-големия елемент под 2,5 секунди

Производителността на уеб пряко влияе върху потребителското изживяване, процента на конверсия и класирането в търсачките. Бавните страници губят посетители, преди те изобщо да видят съдържанието ви, а всяка допълнителна секунда чакане подкопава доверието. Ето как да направите сайта си бърз и да го запазите такъв.

Това ръководство тръгва от измерването навън: първо определете какво означава „бързо“, след това поправете най-големите виновници (изображения, шрифтове, JavaScript) и накрая направете подобренията трайни с кеширане и наблюдение.

Core Web Vitals

Core Web Vitals на Google — Largest Contentful Paint (LCP), Interaction to Next Paint (INP) и Cumulative Layout Shift (CLS) — предоставят измерими критерии за потребителското изживяване. Целете се в LCP под 2,5 секунди, INP под 200 милисекунди и CLS под 0,1. През март 2024 г. INP замени First Input Delay (FID) като Core Web Vital и е много по-строг показател: FID измерваше само забавянето, преди да бъде обработено първото взаимодействие, докато INP разглежда закъснението на взаимодействията през цялото посещение.

Праговете се оценяват при 75-ия персентил на зарежданията на страниците, поотделно за мобилни и настолни устройства. Този детайл е важен. Страница, която е бърза на лаптопа ви за разработка през офисния Wi-Fi, все пак може да не покрие изискванията, ако една четвърт от реалните ви посетители са на телефони с Android от средния клас в претоварена мрежа.

Какво всъщност ви казва всеки показател

  • LCP измерва кога приключва изобразяването на най-голямото изображение или текстов блок в изгледа. Обикновено това е hero изображение, заглавие или голям абзац. Лошият LCP най-често се дължи на бавен отговор на сървъра, блокиращи изобразяването CSS и JavaScript или hero изображение, което браузърът открива твърде късно.
  • INP измерва колко бързо страницата реагира визуално след кликване, докосване или натискане на клавиш. Лошият INP почти винаги означава, че основната нишка е заета: дълги задачи на JavaScript, тежка хидратация или скъпи обработчици на събития.
  • CLS измерва неочакваното разместване на видимото съдържание. Обичайните виновници са изображения и вградено съдържание без размери, уеб шрифтове, които се зареждат с различни метрики, и банери или реклами, вмъкнати над съществуващото съдържание.

Лабораторни данни срещу данни от реални потребители

Лабораторните инструменти като Lighthouse изпълняват едно синтетично зареждане на симулирано устройство. Те са отлични за отстраняване на проблеми и за улавяне на регресии в pull request, но не могат да измерят INP директно, защото никой не взаимодейства със страницата. Данните от реални потребители, събрани от действителни посетители, са това, на което всъщност се базира оценката на Core Web Vitals. Използвайте лабораторните данни, за да откривате проблеми, а данните от реални потребители — за да решите кои проблеми имат значение.

Започнете от сървъра

Никаква настройка на фронтенда не може да скрие бавен първи байт. Time to First Byte (TTFB) не е Core Web Vital, но стои непосредствено преди LCP: браузърът не може да започне изобразяването, докато не пристигне HTML.

  • Рендерирайте предварително каквото можете. Статичното генериране сервира HTML директно от CDN без обиколка до основния сървър. За страниците, които наистина се нуждаят от данни при всяка заявка, кеширайте рендерирания резултат на ръба на мрежата с кратък живот, вместо да рендерирате всяка заявка от нулата.
  • Предавайте HTML поточно. Когато сървърното рендериране е неизбежно, изпратете рано заглавната част на документа, за да може браузърът да започне да изтегля CSS и шрифтовете, докато тялото все още се генерира.
  • Избягвайте вериги от пренасочвания. Всяко пренасочване е пълна обиколка, преди да се зареди каквото и да е съдържание. Сочете директно към каноничните URL адреси и поддържайте последователни правилата за наклонената черта в края и www.
  • Включете модерните протоколи и компресията. HTTP/2 или HTTP/3 и компресия с Brotli или gzip за текстовите ресурси са задължителен минимум за всеки сериозен хостинг или CDN.

Оптимизация на изображенията

Изображенията остават най-големият фактор за теглото на страницата в повечето сайтове. Използвайте модерни формати като WebP и AVIF с подходящи резервни варианти. Внедрете адаптивни изображения със srcset, за да сервирате подходящо оразмерени изображения за всяко устройство. Зареждайте мързеливо изображенията под видимата част с естествения атрибут loading="lazy".

Правилно оформено адаптивно изображение изглежда така:

<img
  src="/images/dashboard-800.webp"
  srcset="/images/dashboard-400.webp 400w,
          /images/dashboard-800.webp 800w,
          /images/dashboard-1600.webp 1600w"
  sizes="(min-width: 1024px) 50vw, 100vw"
  width="1600"
  height="900"
  alt="Revenue dashboard showing monthly trends"
  loading="lazy"
  decoding="async"
/>

Атрибутите width и height позволяват на браузъра да запази правилното съотношение на страните, преди файлът да се изтегли, което премахва често срещан източник на разместване на оформлението. Атрибутът sizes казва на браузъра колко широко ще се изобрази изображението, за да може да избере най-малкия подходящ кандидат от srcset.

Третирайте LCP изображението по различен начин

Мързеливото зареждане е грешен избор за изображението, което най-вероятно е вашият LCP елемент. Мързеливо зареденото hero изображение изчаква оформлението, преди да започне да се изтегля, което пряко забавя LCP. За hero изображението:

  • Премахнете loading="lazy".
  • Добавете fetchpriority="high", за да го изтегли браузърът преди другите изображения.
  • Уверете се, че то може да бъде открито в първоначалния HTML, а не е зададено като CSS фон или вмъкнато от JavaScript.
  • Ако трябва да идва от CSS или от компонент, който се зарежда късно, добавете <link rel="preload" as="image"> в заглавната част на документа.

Ако използвате Astro, вградените компоненти <Image /> и <Picture /> генерират оптимизирани формати, размери и стойности за srcset по време на компилиране, което премахва по-голямата част от ръчната работа.

Шрифтове без премигване

Уеб шрифтовете влияят главно на LCP и CLS. Шрифт, който блокира изобразяването на текста, забавя LCP, когато LCP елементът е текст, а резервен шрифт, който се сменя с уеб шрифт с различни метрики, причинява разместване на оформлението.

  • Хоствайте сами шрифтовете си, за да идват от същия произход и връзка като страницата ви и да избегнете допълнително DNS търсене и TLS ръкостискане.
  • Подбирайте подмножества на шрифтовете само с наборите от знаци, които наистина използвате. Подмножество само с латиница е малка част от размера на пълен многоезичен файл.
  • Използвайте само WOFF2; всеки съвременен браузър го поддържа.
  • Зареждайте предварително само един или два файла с шрифтове, използвани във видимата част.
  • Съгласувайте метриките на резервния шрифт с дескрипторите size-adjust, ascent-override и descent-override, така че смяната да бъде почти незабележима.
@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
}

body {
  font-family: "Inter", "Inter Fallback", system-ui, sans-serif;
}

Точните стойности зависят от двойката шрифтове; няколко инструмента с отворен код могат да ги изчислят вместо вас.

Разделяне на кода

Изпращайте само JavaScript кода, от който страницата наистина се нуждае. Фреймуърци като Astro се справят с това автоматично чрез островната архитектура, но при приложения с много клиентска логика динамичните импорти и разделянето по маршрути намаляват значително размера на първоначалните пакети.

Байтовете са само половината от цената. JavaScript трябва също да бъде парснат, компилиран и изпълнен в основната нишка, а на телефон от средния клас тази работа може да отнеме няколко пъти повече време, отколкото на машината на разработчика. Затова големите пакети се проявяват като лош INP, а не само като бавно зареждане.

Зареждайте интерактивния код, когато е нужен

Отлагайте всичко, което не е необходимо за първото взаимодействие. Библиотека за графики, използвана в модален прозорец, няма място в първоначалния пакет:

const openReport = async () => {
  const { renderChart } = await import("./charts/revenue-chart");
  renderChart(document.querySelector("#report")!);
};

button.addEventListener("click", openReport);

В Astro избирайте директивата за хидратация съзнателно. client:load хидратира веднага, client:idle изчаква, докато браузърът се освободи, а client:visible изчаква, докато компонентът влезе в изгледа при превъртане. Уиджет за коментари в края на статия почти никога не се нуждае от client:load.

Проверете зависимостите си

Пускайте анализатор на пакетите при всяка продукционна компилация и търсете обичайните заподозрени: цяла помощна библиотека, импортирана заради една функция, библиотеки за дати, които включват всеки локал, дублирани копия на един и същ пакет в различни версии и полифили за браузъри, които вече не поддържате. Замяната на една тежка зависимост често спестява повече от седмици микрооптимизации.

Поддържайте основната нишка свободна

Проблемите с INP са проблеми на основната нишка. Всяка задача, която се изпълнява по-дълго от 50 милисекунди, е дълга задача и докато тя тече, браузърът не може да реагира на въвеждане.

  • Разделяйте дългите задачи. Разделете големите цикли и процедурите по инициализация на части и връщайте контрола на браузъра между тях. scheduler.yield() е наличен в браузърите, базирани на Chromium, и във Firefox; проверявайте дали се поддържа и навсякъде другаде използвайте резервен вариант със setTimeout.
  • Правете по-малко в обработчиците на събития. Обновете интерфейса веднага, след което отложете извикванията към аналитиката, записването и невидимата работа.
  • Избягвайте пренатоварването на оформлението. Четенето на свойства на оформлението като offsetHeight след запис на стилове предизвиква синхронно преизчисляване на оформлението. Групирайте четенията преди записите.
  • Изнесете тежките изчисления извън основната нишка. Парсването на големи JSON данни, филтрирането на големи набори от данни или обработката на изображения могат да се изпълняват в Web Worker.
  • Следете скриптовете на трети страни. Мениджърите на тагове, чат уиджетите и инструментите за A/B тестване често причиняват най-лошите дълги задачи на страницата. Зареждайте ги, след като страницата стане интерактивна, и се питайте дали всеки от тях все още оправдава цената си.
async function processInChunks<T>(items: T[], work: (item: T) => void) {
  for (let i = 0; i < items.length; i++) {
    work(items[i]!);
    if (i % 100 === 0) {
      await new Promise((resolve) => setTimeout(resolve, 0));
    }
  }
}

CSS и изобразяване

Стиловите файлове в заглавната част на документа по замисъл блокират изобразяването. Обикновено това е правилно, защото не искате да премигва нестилизирано съдържание, но означава, че размерът на CSS пряко влияе на LCP.

  • Поддържайте критичния CSS малък. Тук помагат utility-first фреймуърците, които генерират само използваните от вас класове.
  • Избягвайте @import в CSS файловете, защото създава последователни вериги от заявки.
  • Използвайте content-visibility: auto за дълги секции под видимата част, за да може браузърът да пропусне работата по изобразяването им, докато не наближат изгледа. Съчетайте го с contain-intrinsic-size, за да избегнете подскачане на лентата за превъртане.
  • Анимирайте transform и opacity вместо свойства като top, width или height, които предизвикват преизчисляване на оформлението при всеки кадър.

Стратегии за кеширане

Внедрете многослоен подход към кеширането: кеш на браузъра за статичните ресурси с дълги заглавки max-age, CDN кеш на ръба на мрежата за географски разпределено съдържание и кеш на приложението за изчислени данни или данни от базата данни. Използвайте обезсилване на кеша чрез хешове на съдържанието за непроменливите ресурси.

Практичен набор от заглавки покрива повечето сайтове:

# Ресурси от компилацията с хеш: никога не се променят, кеширайте завинаги
Cache-Control: public, max-age=31536000, immutable

# HTML: винаги се проверява наново, за да се виждат внедряванията веднага
Cache-Control: public, max-age=0, must-revalidate

# Полудинамични отговори от API: сервирайте остаряла версия, докато се опреснява
Cache-Control: public, max-age=60, stale-while-revalidate=300

Ключовото правило е, че дълъг живот получават само файлове с хеш на съдържанието в името. Ако едно име на файл може да се използва повторно за различно съдържание, дългият max-age ще остави потребителите заседнали на стари версии след внедряване.

Кеширане на ръба и в приложението

На ниво CDN кеширайте HTML за анонимните посетители винаги когато съдържанието го позволява и използвайте surrogate ключове или изчистване по етикети, ако доставчикът ви го поддържа, така че актуализация на съдържанието да обезсилва точно страниците, които засяга. На ниво приложение кеширайте скъпите заявки и изчислените фрагменти и бъдете изрични относно обезсилването. Кеш без ясна стратегия за обезсилване рано или късно започва да сервира грешни данни, което е по-лошо от това да сервира бавни данни.

Ускорете следващата страница

След като текущата страница е бърза, направете навигацията да изглежда мигновена. Изтегляйте предварително вероятните следващи страници при посочване с мишката или когато връзките влязат в изгледа; Astro включва конфигурируема функция за предварително изтегляне точно за това. Уверете се, че страниците отговарят на условията за кеша за назад/напред на браузъра, като избягвате обработчици на unload и Cache-Control: no-store за HTML, който няма нужда от това.

Задайте бюджет за производителност

Без предпазни механизми поправките регресират. Бюджетът за производителност превръща „поддържайте го бързо“ в числа, при които компилацията може да се провали:

  • Максимален обем JavaScript, изпращан за всеки маршрут, в компресиран вид.
  • Максимално общо тегло на изображенията на ключовите целеви страници.
  • Минимален резултат за производителност в Lighthouse на представителни страници.
  • Цели за данните от реални потребители: LCP, INP и CLS при 75-ия персентил в рамките на праговете за „добро“.

Проверявайте лабораторните бюджети в локалната си проверка или в CI при всяка промяна и преглеждайте целите за данните от реални потребители през редовни интервали. Когато даден бюджет бъде надхвърлен, третирайте го като неуспешен тест: или поправете регресията, или вземете изрично, документирано решение да повишите лимита.

Наблюдение

Оптимизацията на производителността не е еднократна задача. Настройте наблюдение на реалните потребители (RUM), за да следите показателите за производителност от действителни потребителски сесии. Комерсиалните RUM услуги или JavaScript библиотеката с отворен код web-vitals осигуряват постоянна видимост за това как реалните потребители изживяват сайта ви.

Самостоятелното събиране на данни от реални потребители отнема само няколко реда:

import { onCLS, onINP, onLCP } from "web-vitals";

function send(metric: { name: string; value: number; id: string }) {
  navigator.sendBeacon("/api/vitals", JSON.stringify(metric));
}

onCLS(send);
onINP(send);
onLCP(send);

Където е възможно, разделяйте данните по шаблон на страницата, тип устройство и връзка. Средната стойност за целия сайт скрива онзи шаблон, който се проваля за повечето от посетителите си. Версията на библиотеката с атрибуция може също да отчете кой елемент е бил кандидатът за LCP и кое взаимодействие е причинило бавния INP, което превръща неясното „сайтът е бавен“ в конкретна поправка.

Заключителни мисли

Бързите сайтове рядко са резултат от един хитър трик. Те се дължат на кратък списък от навици, прилагани последователно: измервайте с данни от реални потребители, сервирайте HTML бързо, приоритизирайте LCP ресурса, изпращайте по-малко JavaScript, поддържайте основната нишка свободна, кеширайте съзнателно и пазете резултатите с бюджет. Започнете с показателя, който е най-далеч от прага си на най-важните ви страници, поправете най-голямата причина, измерете отново и повторете.

Споделете тази статия
performance frontend