← Браузер/Оптимизация производительности#184 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: async и сетьТермин: Event LoopТермин: Core Web Vitals

Оптимизация производительности

Производительность — это функция. Медленный сайт теряет пользователей: по данным Google, каждая дополнительная секунда загрузки снижает конверсию на 7%. Понимание метрик и техник оптимизации — это разница между сайтом, который работает, и сайтом, который продаёт.

Core Web Vitals

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. Измеряет стабильность разметки.

Debounce и Throttle

Быстрые события (scroll, resize, input) могут вызывать обработчики сотни раз в секунду. Это перегружает CPU и вызывает лагание.

Debounce — подождать N миллисекунд после последнего события, потом выполнить. Подходит для поиска по мере ввода: не делать запрос после каждой буквы.

Throttle — выполнять не чаще чем раз в N миллисекунд. Подходит для scroll-обработчиков и событий мыши.

Lazy Loading (ленивая загрузка)

Загружать ресурсы только когда они нужны:

  • Изображения: <img loading="lazy"> — нативная ленивая загрузка
  • Изображения через IntersectionObserver — загружать когда элемент входит во viewport
  • Модули: import('./heavyModule.js') — динамический импорт по требованию
  • Компоненты: route-based code splitting в React/Vue
  • IntersectionObserver

    IntersectionObserver отслеживает, когда элемент входит или выходит из области видимости (viewport или другого контейнера). Это асинхронный API — не блокирует основной поток и не вызывает forced layout.

    Применения: ленивая загрузка, бесконечная прокрутка, анимации при появлении, аналитика видимости.

    Virtual Scrolling

    При рендеринге тысяч элементов список становится тормозным. Virtual scrolling: рендеришь только видимые элементы (~10-20), а остальные — видимость через пустые div-заглушки. При прокрутке элементы переиспользуются. Применяется в таблицах с тысячами строк.

    60fps и will-change

    Браузер обновляет экран ~60 раз в секунду (60fps). Каждый кадр — 16.7мс. За это время должны выполниться JS + Layout + Paint + Composite.

    CSS свойство will-change: transform заранее сообщает браузеру, что элемент будет анимирован. Браузер создаёт для него отдельный GPU-слой заблаговременно, что ускоряет анимацию. Но не злоупотребляй: каждый GPU-слой потребляет видеопамять.

    Performance Budget

    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%. Понимание метрик и техник оптимизации — это разница между сайтом, который работает, и сайтом, который продаёт.

    Core Web Vitals

    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. Измеряет стабильность разметки.

    Debounce и Throttle

    Быстрые события (scroll, resize, input) могут вызывать обработчики сотни раз в секунду. Это перегружает CPU и вызывает лагание.

    Debounce — подождать N миллисекунд после последнего события, потом выполнить. Подходит для поиска по мере ввода: не делать запрос после каждой буквы.

    Throttle — выполнять не чаще чем раз в N миллисекунд. Подходит для scroll-обработчиков и событий мыши.

    Lazy Loading (ленивая загрузка)

    Загружать ресурсы только когда они нужны:

  • Изображения: <img loading="lazy"> — нативная ленивая загрузка
  • Изображения через IntersectionObserver — загружать когда элемент входит во viewport
  • Модули: import('./heavyModule.js') — динамический импорт по требованию
  • Компоненты: route-based code splitting в React/Vue
  • IntersectionObserver

    IntersectionObserver отслеживает, когда элемент входит или выходит из области видимости (viewport или другого контейнера). Это асинхронный API — не блокирует основной поток и не вызывает forced layout.

    Применения: ленивая загрузка, бесконечная прокрутка, анимации при появлении, аналитика видимости.

    Virtual Scrolling

    При рендеринге тысяч элементов список становится тормозным. Virtual scrolling: рендеришь только видимые элементы (~10-20), а остальные — видимость через пустые div-заглушки. При прокрутке элементы переиспользуются. Применяется в таблицах с тысячами строк.

    60fps и will-change

    Браузер обновляет экран ~60 раз в секунду (60fps). Каждый кадр — 16.7мс. За это время должны выполниться JS + Layout + Paint + Composite.

    CSS свойство will-change: transform заранее сообщает браузеру, что элемент будет анимирован. Браузер создаёт для него отдельный GPU-слой заблаговременно, что ускоряет анимацию. Но не злоупотребляй: каждый GPU-слой потребляет видеопамять.

    Performance Budget

    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.

    Загружаем среду выполнения...
    Загружаем AI-помощника...