Как предотвратить перезапись фона при скролле в JavaScript
Исправляем перезапись условий в функции смены фона при скролле: быстрый фикс с else if и break, предвычисление границ, IntersectionObserver для производительности. Примеры кода и оптимизация с throttle.
Как предотвратить перезапись условий друг другом в JavaScript функции смены фона при скролле?
В функции shiftBackground при обработке массива changes условия if для установки backgroundImage последовательно перезаписывают друг друга. Нужно сделать так, чтобы применялось правильное изображение в зависимости от позиции скролла для каждой секции.
Вот текущий код:
function shiftBackground(changes) {
changes.forEach(el => {
const boxs = el[0];
const pathImg = el[1];
const pathImg_defalt = el[2];
const _box = el[3];
const back = document.getElementById('back');
let scrollPosition = window.scrollY;
let shift = 0;
const _box_style = window.getComputedStyle(document.getElementById(_box), null);
const _box_margins = parseFloat(_box_style.marginTop) + parseFloat(_box_style.marginBottom);
const _box_hight = document.getElementById(_box).offsetHeight + _box_margins;
boxs.forEach(box => {
const style = window.getComputedStyle(document.getElementById(box), null);
const margins = parseFloat(style.marginTop) + parseFloat(style.marginBottom);
const hight = document.getElementById(box).offsetHeight;
shift += hight + margins;
});
if(scrollPosition < shift){
back.style.backgroundImage = `url(${pathImg_defalt})`;
}
if(scrollPosition >= shift && scrollPosition < shift + _box_hight){
back.style.backgroundImage = `url(${pathImg})`;
}
})
}
window.addEventListener('scroll', () => {
shiftBackground([[['header', 'main'], '/img/back/back_2.jpg', '/img/back/back.avif', 'about'],
[['header', 'main', 'scills_box', 'about'], '/img/back/back.jpg', '/img/back/back.avif', 'contacts']]);
});
Как исправить логику, чтобы условия работали корректно без перезаписи?
Причина перезаписи — вы проходите весь массив changes и для каждой итерации снова пишете back.style.backgroundImage, поэтому более поздние элементы перезаписывают результат. Простые варианты решения: заменить forEach на цикл, который можно прервать (for / for…of + break), или заранее вычислить диапазоны и выбрать только одну подходящую секцию; оптимально — использовать IntersectionObserver для смены фона при скролле. Дополнительно кешируйте размеры и ограничьте частоту срабатываний (throttle / requestAnimationFrame), чтобы не тратить ресурсы и не получать мерцаний фона.
Содержание
- Почему фон перезаписывается
- Быстрый фикс — заменить forEach на for/for…of и break
- Правильный расчёт границ секций (getBoundingClientRect / offsetTop)
- IntersectionObserver — лучший вариант для смены фона при скролле
- Оптимизация производительности: debounce/throttle, requestAnimationFrame
- Примеры кода: исправленный shiftBackground и вариант с IntersectionObserver
- Источники
- Заключение
Почему фон перезаписывается
В вашем коде функция проходит по массиву changes и в каждой итерации ставит back.style.backgroundImage. Даже если внутри каждой итерации вы используете корректные условия, последующие итерации могут снова записать фон — вот и получается перезапись. К тому же forEach нельзя прервать через break (его нужно заменять на for/for…of, если хотите прервать обработку), подробнее — GeeksforGeeks.
Ещё одна проблема — вычисление “shift” и границ секций. Лучше явно иметь диапазон начала/конца для каждой секции (абсолютные координаты) или использовать IntersectionObserver, чтобы не гадать с накопительными суммами высот.
Быстрый фикс — else if, for/for…of и break (смена фона при скролле)
Самый быстрый патч — пройти массив не через forEach, а через цикл, и прерывать цикл, как только найдено совпадение. Тогда дальнейшие элементы не будут перезаписывать фон.
Пример минимального исправления (минимально меняет вашу логику):
const changes = [
[['header', 'main'], '/img/back/back_2.jpg', '/img/back/back.avif', 'about'],
[['header', 'main', 'scills_box', 'about'], '/img/back/back.jpg', '/img/back/back.avif', 'contacts']
];
function shiftBackground(changes) {
const back = document.getElementById('back');
const scrollPosition = window.scrollY || window.pageYOffset;
for (const el of changes) {
const [boxs, pathImg, pathImg_default, targetId] = el;
// вычисляем "shift" (верхнюю границу целевой секции)
let shift = 0;
for (const box of boxs) {
const node = document.getElementById(box);
if (!node) continue;
const style = getComputedStyle(node);
const margins = (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);
shift += node.offsetHeight + margins;
}
const targetNode = document.getElementById(targetId);
if (!targetNode) continue;
const targetStyle = getComputedStyle(targetNode);
const targetMargins = (parseFloat(targetStyle.marginTop) || 0) + (parseFloat(targetStyle.marginBottom) || 0);
const targetHeight = targetNode.offsetHeight + targetMargins;
if (scrollPosition < shift) {
back.style.backgroundImage = `url(${pathImg_default})`;
break; // важно: прекращаем дальнейшую обработку changes
} else if (scrollPosition >= shift && scrollPosition < shift + targetHeight) {
back.style.backgroundImage = `url(${pathImg})`;
break; // также прекращаем — найден подходящий диапазон
}
// иначе — идём к следующему элементу changes
}
}
Ключевые идеи: 1) for/for…of позволяет сделать break; 2) после установки фона делаем break — дальше перезаписывать никто не будет; 3) оставляем логику расчёта shift, но лучше заменить её на более надёжный расчёт координат (см. далее).
Правильный расчёт границ секций (getBoundingClientRect / offsetTop)
Суммирование высот коробок работает, но хрупко (отступы, скрытые элементы, динамический контент). Надёжнее вычислять абсолютные координаты секции:
- start = elem.getBoundingClientRect().top + window.scrollY
- end = start + elem.offsetHeight + marginTop + marginBottom
Лучше один раз (при загрузке и при ресайзе) собрать массив диапазонов и изображений, а в обработчике скролла просто искать подходящий диапазон (find). Это экономит время и делает код предсказуемым.
Пример предвычисления:
function buildRanges(changes) {
return changes.map(([boxs, pathImg, pathImg_default, targetId]) => {
const node = document.getElementById(targetId);
if (!node) return null;
const rect = node.getBoundingClientRect();
const start = rect.top + window.scrollY;
const style = getComputedStyle(node);
const margins = (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);
const end = start + node.offsetHeight + margins;
return { start, end, pathImg, pathImg_default };
}).filter(Boolean);
}
Не забудьте пересобирать ranges при изменении контента или размере окна.
(За детали про getBoundingClientRect можно посмотреть на статью — Medium: How getBoundingClientRect works.)
IntersectionObserver — лучший вариант для смены фона при скролле
Если цель — менять фон именно когда секция входит в видимую область, то IntersectionObserver — более надёжный и производительный способ: браузер сам решает, когда вызывать колбэк, и вы избегаете частых вычислений в scroll. Практическое руководство и примеры — html-plus.in.ua.
Простой пример: добавьте data-атрибут с путём картинки и наблюдайте за секциями:
HTML:
<section id="about" data-bg="/img/back/back_2.jpg">...</section>
<section id="contacts" data-bg="/img/back/back.jpg">...</section>
JS:
const back = document.getElementById('back');
const sections = document.querySelectorAll('[data-bg]');
const observer = new IntersectionObserver((entries) => {
// выбираем наиболее видимую секцию среди тех, что пересеклись
const visible = entries.filter(e => e.isIntersecting);
if (visible.length === 0) return;
visible.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
back.style.backgroundImage = `url(${visible[0].target.dataset.bg})`;
}, { root: null, rootMargin: '0px', threshold: [0, 0.25, 0.5, 0.75, 1] });
sections.forEach(s => observer.observe(s));
Плюсы: меньше перезаписей, более точное поведение при перекрытиях, не нужно вручную считать offsets. Минус: старые браузеры требуют polyfill.
Оптимизация производительности: debounce/throttle, requestAnimationFrame
Обработчик scroll срабатывает очень часто — десятки раз в секунду. Если вы каждый раз пересчитываете размеры и устанавливаете стиль, будут просадки FPS и лишние перезаписи. Рекомендации:
- Ограничьте частоту вызова — throttle или requestAnimationFrame (ticking pattern). Purpleschool подробно объясняет проблему частых вызовов scroll-обработчика: Purpleschool: событие scroll.
- Кешируйте DOM-узлы и предвычисленные диапазоны.
- Пересчитывайте диапазоны по событию resize, а не на каждый scroll.
Пример с requestAnimationFrame:
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
shiftBackground(changes); // либо updateFromRanges(ranges)
ticking = false;
});
ticking = true;
}
});
Примеры кода: исправленный shiftBackground и вариант с IntersectionObserver
- Исправленный shiftBackground (минимальный, с break и rAF):
const changes = [ /* как у вас */ ];
function shiftBackground(changes) {
const back = document.getElementById('back');
const scrollY = window.scrollY || window.pageYOffset;
for (const el of changes) {
const [boxs, pathImg, pathImg_default, targetId] = el;
let shift = 0;
for (const box of boxs) {
const node = document.getElementById(box);
if (!node) continue;
const style = getComputedStyle(node);
const margins = (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);
shift += node.offsetHeight + margins;
}
const target = document.getElementById(targetId);
if (!target) continue;
const targetStyle = getComputedStyle(target);
const targetHeight = target.offsetHeight + (parseFloat(targetStyle.marginTop) || 0) + (parseFloat(targetStyle.marginBottom) || 0);
if (scrollY < shift) {
back.style.backgroundImage = `url(${pathImg_default})`;
break;
} else if (scrollY >= shift && scrollY < shift + targetHeight) {
back.style.backgroundImage = `url(${pathImg})`;
break;
}
}
}
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
shiftBackground(changes);
ticking = false;
});
ticking = true;
}
});
- Вариант с IntersectionObserver (рекомендуемый):
HTML: дать секциям data-bg, как показано выше.
JS:
const back = document.getElementById('back');
const sections = document.querySelectorAll('[data-bg]');
const observer = new IntersectionObserver((entries) => {
const visible = entries.filter(e => e.isIntersecting);
if (visible.length === 0) return;
visible.sort((a,b) => b.intersectionRatio - a.intersectionRatio);
back.style.backgroundImage = `url(${visible[0].target.dataset.bg})`;
}, { threshold: [0, 0.25, 0.5, 0.75, 1] });
sections.forEach(s => observer.observe(s));
Если нужна точная логика “дефолтного” фонового изображения (например, когда ни одна секция не видима) — держите переменную defaultPath и при отсутствии visible ставьте её.
Источники
- Событие scroll в JavaScript — Purpleschool
- Intersection Observer API. Прокрутка контента — html-plus.in.ua
- How to stop forEach() method in JavaScript? — GeeksforGeeks
- Анимация прокрутки в CSS: animation-timeline: scroll() — shra.ru
- How getBoundingClientRect Works and What It Returns — Medium
Заключение
Коротко: чтобы избежать перезаписи условий при смене фона при скролле — не позволяйте циклу продолжать обработку после нахождения подходящей секции (замена forEach на for/for…of + break), либо заранее вычислите диапазоны и выбирайте одну подходящую запись, либо (ещё лучше) используйте IntersectionObserver. И не забывайте кешировать размеры и ограничивать частоту обновлений (rAF / throttle), чтобы эффект был плавным и надёжным.