Производительность — это функция. Медленный сайт теряет пользователей: по данным Google, каждая дополнительная секунда загрузки снижает конверсию на 7%. Понимание метрик и техник оптимизации — это разница между сайтом, который работает, и сайтом, который продаёт.
Google ввёл три ключевые метрики, которые влияют на ранжирование в поиске:
LCP (Largest Contentful Paint) — когда отрисован главный контент. Хорошо: < 2.5s. Измеряет воспринимаемую скорость загрузки.
FID/INP (First Input Delay / Interaction to Next Paint) — задержка ответа на первое взаимодействие (FID) или любое взаимодействие (INP, заменил FID в 2024). Хорошо: < 200мс. Измеряет отзывчивость.
CLS (Cumulative Layout Shift) — суммарный сдвиг элементов страницы без действия пользователя (когда кнопка уезжает прямо перед кликом). Хорошо: < 0.1. Измеряет стабильность разметки.
Быстрые события (scroll, resize, input) могут вызывать обработчики сотни раз в секунду. Это перегружает CPU и вызывает лагание.
Debounce — подождать N миллисекунд после последнего события, потом выполнить. Подходит для поиска по мере ввода: не делать запрос после каждой буквы.
Throttle — выполнять не чаще чем раз в N миллисекунд. Подходит для scroll-обработчиков и событий мыши.
Загружать ресурсы только когда они нужны:
<img loading="lazy"> — нативная ленивая загрузкаimport('./heavyModule.js') — динамический импорт по требованиюIntersectionObserver отслеживает, когда элемент входит или выходит из области видимости (viewport или другого контейнера). Это асинхронный API — не блокирует основной поток и не вызывает forced layout.
Применения: ленивая загрузка, бесконечная прокрутка, анимации при появлении, аналитика видимости.
При рендеринге тысяч элементов список становится тормозным. Virtual scrolling: рендеришь только видимые элементы (~10-20), а остальные — видимость через пустые div-заглушки. При прокрутке элементы переиспользуются. Применяется в таблицах с тысячами строк.
Браузер обновляет экран ~60 раз в секунду (60fps). Каждый кадр — 16.7мс. За это время должны выполниться JS + Layout + Paint + Composite.
CSS свойство will-change: transform заранее сообщает браузеру, что элемент будет анимирован. Браузер создаёт для него отдельный GPU-слой заблаговременно, что ускоряет анимацию. Но не злоупотребляй: каждый GPU-слой потребляет видеопамять.
Performance Budget — лимиты, которые нельзя превышать: размер JS-бандла, время до интерактивности, LCP. Это встраивается в CI/CD: PR отклоняется, если бандл вырастет больше допустимого.
Вместо одного большого JS-файла — несколько маленьких. Пользователь загружает только код для текущей страницы. React.lazy(), динамический import(), route-level splitting в Next.js/Nuxt.
Debounce, throttle, IntersectionObserver для ленивой загрузки, измерение CLS
// Debounce — выполнить функцию через N мс после последнего вызова
function debounce(fn, delay) {
let timerId
return function (...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
// Throttle — выполнять не чаще чем раз в N мс
function throttle(fn, interval) {
let lastCall = 0
return function (...args) {
const now = Date.now()
if (now - lastCall >= interval) {
lastCall = now
return fn.apply(this, args)
}
}
}
// Применение: поиск с debounce
const searchInput = document.querySelector('#search')
const search = debounce((query) => {
console.log('Поиск:', query) // запрос только после паузы в 300мс
}, 300)
searchInput?.addEventListener('input', e => search(e.target.value))
// Throttle для scroll
const onScroll = throttle(() => {
console.log('Scroll Y:', window.scrollY) // не чаще раза в 100мс
}, 100)
window.addEventListener('scroll', onScroll)
// IntersectionObserver — ленивая загрузка изображений
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
// Подменяем data-src на src — изображение начнёт загружаться
img.src = img.dataset.src
img.removeAttribute('data-src')
observer.unobserve(img) // больше не следим за этим элементом
console.log('Загружаем картинку:', img.src)
}
})
}, { rootMargin: '200px' }) // начинаем за 200px до попадания во viewport
// Наблюдаем за всеми ленивыми картинками
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img)
})
// Измерение CLS через PerformanceObserver
const clsObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) { // не считаем сдвиги от действий пользователя
console.log('Сдвиг разметки:', entry.value.toFixed(4))
}
})
})
clsObserver.observe({ type: 'layout-shift', buffered: true })Производительность — это функция. Медленный сайт теряет пользователей: по данным Google, каждая дополнительная секунда загрузки снижает конверсию на 7%. Понимание метрик и техник оптимизации — это разница между сайтом, который работает, и сайтом, который продаёт.
Google ввёл три ключевые метрики, которые влияют на ранжирование в поиске:
LCP (Largest Contentful Paint) — когда отрисован главный контент. Хорошо: < 2.5s. Измеряет воспринимаемую скорость загрузки.
FID/INP (First Input Delay / Interaction to Next Paint) — задержка ответа на первое взаимодействие (FID) или любое взаимодействие (INP, заменил FID в 2024). Хорошо: < 200мс. Измеряет отзывчивость.
CLS (Cumulative Layout Shift) — суммарный сдвиг элементов страницы без действия пользователя (когда кнопка уезжает прямо перед кликом). Хорошо: < 0.1. Измеряет стабильность разметки.
Быстрые события (scroll, resize, input) могут вызывать обработчики сотни раз в секунду. Это перегружает CPU и вызывает лагание.
Debounce — подождать N миллисекунд после последнего события, потом выполнить. Подходит для поиска по мере ввода: не делать запрос после каждой буквы.
Throttle — выполнять не чаще чем раз в N миллисекунд. Подходит для scroll-обработчиков и событий мыши.
Загружать ресурсы только когда они нужны:
<img loading="lazy"> — нативная ленивая загрузкаimport('./heavyModule.js') — динамический импорт по требованиюIntersectionObserver отслеживает, когда элемент входит или выходит из области видимости (viewport или другого контейнера). Это асинхронный API — не блокирует основной поток и не вызывает forced layout.
Применения: ленивая загрузка, бесконечная прокрутка, анимации при появлении, аналитика видимости.
При рендеринге тысяч элементов список становится тормозным. Virtual scrolling: рендеришь только видимые элементы (~10-20), а остальные — видимость через пустые div-заглушки. При прокрутке элементы переиспользуются. Применяется в таблицах с тысячами строк.
Браузер обновляет экран ~60 раз в секунду (60fps). Каждый кадр — 16.7мс. За это время должны выполниться JS + Layout + Paint + Composite.
CSS свойство will-change: transform заранее сообщает браузеру, что элемент будет анимирован. Браузер создаёт для него отдельный GPU-слой заблаговременно, что ускоряет анимацию. Но не злоупотребляй: каждый GPU-слой потребляет видеопамять.
Performance Budget — лимиты, которые нельзя превышать: размер JS-бандла, время до интерактивности, LCP. Это встраивается в CI/CD: PR отклоняется, если бандл вырастет больше допустимого.
Вместо одного большого JS-файла — несколько маленьких. Пользователь загружает только код для текущей страницы. React.lazy(), динамический import(), route-level splitting в Next.js/Nuxt.
Debounce, throttle, IntersectionObserver для ленивой загрузки, измерение CLS
// Debounce — выполнить функцию через N мс после последнего вызова
function debounce(fn, delay) {
let timerId
return function (...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
// Throttle — выполнять не чаще чем раз в N мс
function throttle(fn, interval) {
let lastCall = 0
return function (...args) {
const now = Date.now()
if (now - lastCall >= interval) {
lastCall = now
return fn.apply(this, args)
}
}
}
// Применение: поиск с debounce
const searchInput = document.querySelector('#search')
const search = debounce((query) => {
console.log('Поиск:', query) // запрос только после паузы в 300мс
}, 300)
searchInput?.addEventListener('input', e => search(e.target.value))
// Throttle для scroll
const onScroll = throttle(() => {
console.log('Scroll Y:', window.scrollY) // не чаще раза в 100мс
}, 100)
window.addEventListener('scroll', onScroll)
// IntersectionObserver — ленивая загрузка изображений
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
// Подменяем data-src на src — изображение начнёт загружаться
img.src = img.dataset.src
img.removeAttribute('data-src')
observer.unobserve(img) // больше не следим за этим элементом
console.log('Загружаем картинку:', img.src)
}
})
}, { rootMargin: '200px' }) // начинаем за 200px до попадания во viewport
// Наблюдаем за всеми ленивыми картинками
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img)
})
// Измерение CLS через PerformanceObserver
const clsObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) { // не считаем сдвиги от действий пользователя
console.log('Сдвиг разметки:', entry.value.toFixed(4))
}
})
})
clsObserver.observe({ type: 'layout-shift', buffered: true })Реализуй IntersectionObserver, который отслеживает все элементы с классом "lazy-image". Когда элемент входит во viewport, установи ему атрибут src из data-src и добавь класс "loaded". Используй rootMargin: "100px" чтобы начинать загрузку чуть заранее.
entry.isIntersecting — true когда элемент виден. Исходный URL в data-src доступен через img.dataset.src. Устанавливай src через img.src = src. Прекрати наблюдение через observer.unobserve(img). rootMargin: "100px" означает отступ 100px от края viewport.