0

The end goal is to add a scrolling-up class to the body element when the user is scrolling up the page and remove said class when scrolling down.

I am achieving this the old school way, by attaching a throttled(via lodash) callback to the scroll event, like so:

var lastScrollTop = 0;

var checkScrollDirection = function() {
    var currentOffset = window.pageYOffset;
    document.body.classList.toggle('scrolling-up', currentOffset < lastScrollTop );
    lastScrollTop = currentOffset;
};

window.addEventListener('scroll', _.throttle(
        checkScrollDirection,
        100,
        {
            'leading': true,
            'trailing': true
        }
    )
);

This works (very) well, but I am wondering if it is possible to achieve this by using a modern Observer, and therefore taking all this off the main thread. Even if throttled, the above logic still takes alot more CPU time than makes sense from a purely logical standpoint.

Thank you!

Hlsg
  • 3,213
  • 2
  • 12
  • 17
  • Try IntersectionObserver but there's no guarantee it'll consume less resources. Such things should be measured, never guessed. Also note that adding/removing a class on body will invalidate/rebuild the entire page layout internally, which might be very slow if there many classes/elements that are affected by the changed class (this is also something that can be measured in devtools profiler). – wOxxOm Mar 07 '21 at 15:03
  • This [link]https://itnext.io/1v1-scroll-listener-vs-intersection-observers-469a26ab9eb6 seems to have some interesting results. However, they may not be applicable to your case where you are interested in direction change and as you are looking every 10th of a second at the moment you assumably want it to be very responsive to any change rather than only be interested in, say, changes which are greater than some threshold in terms of distance? – A Haworth Mar 07 '21 at 15:23
  • I gave `body` as an example, and I'm only doing GPU accelerated transforms, so I'm well aware of the implications. My question was about **how** one woudl use an observer to achieve the same result as the given code. – Hlsg Mar 07 '21 at 15:23
  • @AHaworth At the moment I'm hiding and showing a hader based on whether the user is scrolling up or not. Classic stuff. But yes, I want to be responsive. – Hlsg Mar 07 '21 at 15:25
  • @AHaworth Thanks for the link. Nice find, And I think it 100% applies here. But, judgement of performance aside, What I'm askign is how to do this, not wheteher it should be done. – Hlsg Mar 07 '21 at 15:33

1 Answers1

0

If we place some target elements strategically in the body we can have them observed by an IntersectionObserver to sense whether the body is being scrolled up or down.

The minimum number of such targets would seem to be one viewport height one every other 100vh going down the body (with an adjustment for a remaining bit at the end). This way there is always one, but only one, target in the viewport at any one time. This target can be observed so it triggers code at various thresholds.

There is a balance to be struck between the number of 'stops' in the viewport and the time taken by observing. This snippet has observation every 5% of the viewport and on the devices tried (laptop, iPad) this seems to give no perceptible problem in practice.

This method has a slightly hacky feel to it as it entails adding elements to the document, and on a resize we have to resort to JS to recalculate their number, height and position.

However, the method seems to work, the absolute maximum GPU usage I saw with manic continuous scrolling and change of direction was around 7%. 'Ordinary' sort of scrolling hardly registers. The targets in the snippet have width 1px, I do not know whether it's better or worse processor-usage wise to have them wide or thin. They are placed in the center of the viewport just in case there are any problems with observing right at the edges.

This snippet just adds class scrolling-up to the body as appropriate and this shows/hides a fixed header.

//Set up action on intersection being observed
let previousTarget, previousTop, scrollingUp;
const header = document.querySelector('.header');
function scrolledFn(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting) { //note, only one can intersect at a time
      if (entry.target == previousTarget) {
        const newScrolling = (entry.boundingClientRect.top > previousTop);
        if (newScrolling != scrollingUp) {
          document.body.classList.toggle('scrolling-up');
          scrollingUp = !scrollingUp;
        }
      }
      else {
        previousTarget = entry.target;
      }
      previousTop = entry.boundingClientRect.top;
    }
  });
}

const step = 0.05;
let thresholds =[];
for ( let t = 0; t <= 1; t += step) { thresholds.push(t); }
let observer = new IntersectionObserver(scrolledFn, {threshold: thresholds});

function setupTargets() {

// first remove any targets we may have already
const oldTargets = document.querySelectorAll('.observed');
  oldTargets.forEach(oldTarget => { oldTarget.remove();
});

// Insert targets into the document
const numWholeViewports = Math.floor(document.body.offsetHeight/window.innerHeight);
const numFullHeightTargets = Math.floor((numWholeViewports + 1) / 2);

let i = 0;
function createTarget(h) {
  const el = document.createElement('div');
  document.body.appendChild(el);
  observer.observe(el);
  el.classList.add('observed');
  el.style.top = i*200 + 'vh';
  el.style.height = h + 'vh';
}
for (i; i < numFullHeightTargets ; i++) {
  createTarget('100');
}
if (numWholeViewports%2 == 0) { createTarget((document.body.offsetHeight%window.innerHeight) * 100 / window.innerHeight); }

previousTop = 0;
scrollingUp = document.body.scrollTop == 0;
if (scrollingUp) document.body.classList.add('scrolling-up');
else document.body.classList.remove('scrolling-up');
}

window.onload = setupTargets;
window.onresize = setupTargets;
body {
  position: relative;
  width: 100vw;
  height: auto;
  overflow-y: auto;
}

.header {
  display: none;
  position: fixed;
  top: 0px;
  left: 0px;
  background-color: lime;
  width: 100%;
  height: 10%;
  align-items: center;
  justify-content: center;
  font-size: 4rem;
  z-index: 1;
}

.scrolling-up .header {
  display: flex;
}

.content {
  position: relative;
  width: 80vw;
  margin: 0 auto;
  top: 10%;
  padding: 10px 20px;
  font-size: 3rem;
}

.observed {
  width: 1px;
  position: absolute;
  left: 50%;
  margin: 0;
  padding: 0;
  border-width: 0;
  z-index: -99999;
}
<body>
<header class="header">HEADER</header>
<div class="content">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam porttitor erat ut libero molestie, sit amet facilisis libero finibus. Vestibulum tincidunt, augue faucibus faucibus mattis, quam magna sollicitudin ante, sed hendrerit nisl dolor sit amet neque. Integer feugiat malesuada lobortis. Quisque accumsan, nulla efficitur sodales eleifend, purus arcu lacinia nunc, id consequat mauris nisi ultricies lacus. Cras finibus commodo ipsum, in dictum mauris molestie in. Maecenas pretium ipsum ac velit porttitor, eu sagittis sapien vehicula. Donec vulputate urna non dui egestas iaculis.
</p><p>
Etiam sit amet eros in purus venenatis tristique. Donec vel tortor facilisis, tempus nibh a, consectetur velit. Aenean suscipit lacus diam, et ultricies nunc interdum quis. Nam orci sem, hendrerit sit amet lectus nec, convallis facilisis erat. Duis blandit nibh neque, quis porta mauris consectetur sit amet. Nam porttitor dolor vel euismod porttitor. Cras commodo tristique nunc. Proin ultrices sed odio et elementum. Praesent ex dui, placerat sed libero sed, consectetur pellentesque erat. Quisque volutpat molestie nisi eget tristique.
</p><p>
Nam sapien mi, mollis eu scelerisque sit amet, tristique eu purus. In quis feugiat massa. Suspendisse ac tellus neque. Vivamus risus nisl, posuere id sem id, aliquet semper nibh. Sed elementum facilisis bibendum. Maecenas ac nunc placerat lectus ultrices sodales. Nunc nec augue purus. Vestibulum a molestie lacus.
</p><p>
Curabitur ut tortor dolor. Suspendisse semper, leo et luctus laoreet, odio magna sagittis elit, sed bibendum risus lacus ut metus. Nulla a lobortis massa. Pellentesque volutpat iaculis faucibus. Integer vel erat sed orci lobortis rhoncus ornare ut purus. Phasellus rutrum varius rutrum. Curabitur fermentum finibus tortor at placerat. Pellentesque cursus nibh in dolor dictum tristique. Fusce auctor sapien libero, et porta sem pulvinar eu. Praesent lobortis lacus eget lacus fringilla posuere. Mauris vehicula tortor ut elit tincidunt luctus.
</p><p>
Pellentesque porttitor id nulla vitae auctor. Nam orci urna, molestie nec lorem sed, porttitor pulvinar erat. Proin magna sapien, molestie a ipsum eget, iaculis ornare ipsum. Cras imperdiet purus sed sapien sodales sodales. Nam dui nulla, ornare id ornare vel, placerat scelerisque velit. Nunc non dignissim orci. Aliquam diam massa, hendrerit at consectetur eu, eleifend vestibulum erat. Fusce tincidunt eget dolor at faucibus. Donec euismod elementum tellus, eu tristique massa malesuada vel. Aenean sit amet enim id elit sollicitudin dictum.
</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam porttitor erat ut libero molestie, sit amet facilisis libero finibus. Vestibulum tincidunt, augue faucibus faucibus mattis, quam magna sollicitudin ante, sed hendrerit nisl dolor sit amet neque. Integer feugiat malesuada lobortis. Quisque accumsan, nulla efficitur sodales eleifend, purus arcu lacinia nunc, id consequat mauris nisi ultricies lacus. Cras finibus commodo ipsum, in dictum mauris molestie in. Maecenas pretium ipsum ac velit porttitor, eu sagittis sapien vehicula. Donec vulputate urna non dui egestas iaculis.
</p>
</div>

Note, on IOS there may be a slight 'edge' case on scrolling back up to the very top - needs investigating - may be related to 100vh not being the viewport height when there are navbars at the top in the browser.

A Haworth
  • 30,908
  • 4
  • 11
  • 14
  • I thought about something similar and asked this hoping there would be a simpler modern answer. If there is not, I will be sticking to polling scroll. Thakn you for your answer! – Hlsg Mar 12 '21 at 15:25
  • Yes, I didn't like having to add elements (however tiny) but we don't seem to have any other suggestions yet. It is a non CPU/GPU intensive way of doing it though. – A Haworth Mar 12 '21 at 15:35