Современные сайтоделы вообще не парятся об оптимизации фронтэнда – наваливают туда библиотек типа GSAP и юзают их по самые помидоры, из-за чего их красивые сайты начинают нагревать мобильные устройства, да и ноутбуки. Я такого избегаю всеми правдами и неправдами, и наконец-то ChatGPT подсказал мне для этого идеальное решение. Возьмём простой код, который смотрит прокрутил ли пользователь страницу ниже .hero и если да, то вешает класс на body.
$(window).on('scroll', function() {
const heroHeight = $('.hero').outerHeight();
$('body').toggleClass('hero-scrolled', window.scrollY > heroHeight);
});
Довольно распространённая штука для сайтов, которая стреляет десятки раз в секунду при скролле. Как это оптимизировать? Ну, можно попробовать через таймер.
setInterval(() => {
const heroHeight = $('.hero').outerHeight();
$('body').toggleClass('hero-scrolled', window.scrollY > heroHeight);
}, 300);
Теперь эвент стреляет всего 3 раза в секунду. Но, он стреляет каждую секунду, не важно крутится скролл или нет. А теперь к оптимальным решениям.
Debounce:
Эта штука откладывает выполнение события, пока не закончится серия действий (например, пока крутится скролл).
const debounce = (func, delay) => {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
};
$(window).on('scroll', debounce(() => {
const heroHeight = $('.hero').outerHeight();
$('body').toggleClass('hero-scrolled', window.scrollY > heroHeight);
}, 100));
Прелесть тут ещё в том, что эту константу надо задать в своём коде всего раз, а потом её можно использовать для всего – скролл, движение мыши, ресайз окна. Конфликтов не будет, потому что каждое обращение к константе создаёт замкнутую функцию со своим отдельным таймером.
Throttle + Debounce:
Throttle это механизм, который ограничивает частоту вызова функции. Допустим, вы используете только Throttle для отслеживания движения мыши. Задержка означает, что функция начнёт вызываться сразу, как только вы начнёте движение мышью, и дальше она будет вызываться каждые 300 мс (допустим), пока движение продолжается. Но, что если функция выстрелила, ушла на кулдаун на 300мс, а вы после этого двигали курсор ещё 200 мс? Этот последний отрезок не будет обработан. Поэтому механизм Throttle нужно сразу объединять с Debounce.
const throttle = (func, limit) => {
let lastCall = 0;
let timeout; // Для debounce
return function (...args) {
const now = Date.now();
// Вызываем функцию сразу, если прошло `limit` мс
if (now - lastCall >= limit) {
lastCall = now;
func.apply(this, args);
}
// Дебаунс: вызов в конце серии событий (после `limit` мс простоя)
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, limit);
};
};
$(window).on('scroll', throttle(() => {
const heroHeight = $('.hero').outerHeight();
$('body').toggleClass('hero-scrolled', window.scrollY > heroHeight);
}, 300));
Throttle + Debounce + Scroll direction
Можно пойти ещё дальше и подумать над тем, как работает скролл. Он работает вверх и вниз – логично?) То есть, наверно будет неправильно, если мы начали крутить скролл вниз, скрипт ушёл на задержку, а мы в этот момент крутанули скроллом вверх и ничего не произошло? Я думаю, смена направления скролла должна отрабатываться без задержки.
const throttleWithDirection = (func, limit) => {
let lastCall = 0;
let prevScrollY = window.scrollY;
let timeout; // Для debounce
return function (...args) {
const now = Date.now();
const currentScrollY = window.scrollY;
const scrollDown = currentScrollY > prevScrollY;
prevScrollY = currentScrollY;
// Выполняем функцию сразу, если направление изменилось или прошло `limit` мс
if (now - lastCall >= limit || scrollDown !== this.lastDirection) {
lastCall = now;
this.lastDirection = scrollDown;
func.apply(this, args);
}
// Дебаунс: вызов в конце серии событий (после `limit` мс простоя)
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, limit);
};
};
Можно и на этом не успокоиться и сделать объединённую функцию “для всего”. Теперь она принимает 3 параметра. В третьем параметре нужно передать true, если вы отслеживаете скролл и вам нужно также отслеживать моментальную смену направления.
const throttle = (func, limit, scrollMode = false) => {
let lastCall = 0;
let timeout; // Для debounce
let prevScrollY = window.scrollY; // Для отслеживания направления скролла
return function (...args) {
const now = Date.now();
if (scrollMode) {
const currentScrollY = window.scrollY;
const scrollDown = currentScrollY > prevScrollY;
prevScrollY = currentScrollY;
// Если направление изменилось, вызываем сразу
if (scrollDown !== this.lastDirection) {
this.lastDirection = scrollDown;
func.apply(this, args);
}
}
// Вызываем функцию сразу, если прошло `limit` мс
if (now - lastCall >= limit) {
lastCall = now;
func.apply(this, args);
}
// Дебаунс: вызов в конце серии событий (после `limit` мс простоя)
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, limit);
};
};