Веб

Как предотвратить перезапись фона при скролле в JavaScript

Исправляем перезапись условий в функции смены фона при скролле: быстрый фикс с else if и break, предвычисление границ, IntersectionObserver для производительности. Примеры кода и оптимизация с throttle.

Как предотвратить перезапись условий друг другом в JavaScript функции смены фона при скролле?

В функции shiftBackground при обработке массива changes условия if для установки backgroundImage последовательно перезаписывают друг друга. Нужно сделать так, чтобы применялось правильное изображение в зависимости от позиции скролла для каждой секции.

Вот текущий код:

javascript
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), чтобы не тратить ресурсы и не получать мерцаний фона.


Содержание


Почему фон перезаписывается

В вашем коде функция проходит по массиву changes и в каждой итерации ставит back.style.backgroundImage. Даже если внутри каждой итерации вы используете корректные условия, последующие итерации могут снова записать фон — вот и получается перезапись. К тому же forEach нельзя прервать через break (его нужно заменять на for/for…of, если хотите прервать обработку), подробнее — GeeksforGeeks.

Ещё одна проблема — вычисление “shift” и границ секций. Лучше явно иметь диапазон начала/конца для каждой секции (абсолютные координаты) или использовать IntersectionObserver, чтобы не гадать с накопительными суммами высот.


Быстрый фикс — else if, for/for…of и break (смена фона при скролле)

Самый быстрый патч — пройти массив не через forEach, а через цикл, и прерывать цикл, как только найдено совпадение. Тогда дальнейшие элементы не будут перезаписывать фон.

Пример минимального исправления (минимально меняет вашу логику):

javascript
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). Это экономит время и делает код предсказуемым.

Пример предвычисления:

javascript
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:

html
<section id="about" data-bg="/img/back/back_2.jpg">...</section>
<section id="contacts" data-bg="/img/back/back.jpg">...</section>

JS:

javascript
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:

javascript
let ticking = false;
window.addEventListener('scroll', () => {
 if (!ticking) {
 window.requestAnimationFrame(() => {
 shiftBackground(changes); // либо updateFromRanges(ranges)
 ticking = false;
 });
 ticking = true;
 }
});

Примеры кода: исправленный shiftBackground и вариант с IntersectionObserver

  1. Исправленный shiftBackground (минимальный, с break и rAF):
javascript
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;
 }
});
  1. Вариант с IntersectionObserver (рекомендуемый):

HTML: дать секциям data-bg, как показано выше.

JS:

javascript
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 ставьте её.


Источники

  1. Событие scroll в JavaScript — Purpleschool
  2. Intersection Observer API. Прокрутка контента — html-plus.in.ua
  3. How to stop forEach() method in JavaScript? — GeeksforGeeks
  4. Анимация прокрутки в CSS: animation-timeline: scroll() — shra.ru
  5. How getBoundingClientRect Works and What It Returns — Medium

Заключение

Коротко: чтобы избежать перезаписи условий при смене фона при скролле — не позволяйте циклу продолжать обработку после нахождения подходящей секции (замена forEach на for/for…of + break), либо заранее вычислите диапазоны и выбирайте одну подходящую запись, либо (ещё лучше) используйте IntersectionObserver. И не забывайте кешировать размеры и ограничивать частоту обновлений (rAF / throttle), чтобы эффект был плавным и надёжным.

Авторы
Проверено модерацией
Модерация
Как предотвратить перезапись фона при скролле в JavaScript