I have mixed feelings about sticky headers on web pages, but it annoys me more when the implementation causes scroll jank or contributes to it.
The coding pattern needs a few small changes. I'm also posting this under: stuff that's been said before, but is worth saying again.
UK EVENTAttend ffconf.org 2024
The conference for people who are passionate about the web. 8 amazing speakers with real human interaction and content you can't just read in a blog post or watch on a tiktok!
£249+VAT - reserve your place today
The sample starting point
I recently bought an HTML single page template for a project I'm working on, and have been slowly making my way through the code tweaking it to my preferences, when I saw this:
var toggleHeaderFloating = function() {
// Floating Header
if ( $window.scrollTop() > 80 ) {
$( '.header-section' ).addClass( 'floating' );
} else {
$( '.header-section' ).removeClass( 'floating' );
};
};
$window.on( 'scroll', toggleHeaderFloating );
The code will check on every scroll tick whether the scroll position is over 80 pixels, and if it is, it'll add a class (that "floats" the header section) or it will remove the class.
It's fair to assume that $window
is a jQuery instance of the window
object. However, there's a whole bunch of no-nos going on in this code for me. It's not a big deal, but understanding the red flags helps us to understand how to avoid little snags in the future.
Do nothing-to-nothing on scroll
I can't find the original post, but Paul Irish, some many years ago shared insights into scrolling performance, and recommended that inside of the scroll
event (and likely also applies to wheel
and probably mousemove
), that you should avoid touching the DOM and avoid triggering layout (also known as reflows). Paul also collected an excellent list of what triggers layout which is brief enough for me to remember.
If we're doing nothing inside a scroll event, what can we do? We can debounce, using requestAnimationFrame
. When the user scrolls, we'll schedule a function that will check the scroll position, but if the user is scrolling quickly, then that action will take priority, and ideally avoid scroll-jank:
// used to only run on raf call
var rafTimer;
$window.on('scroll', function () {
cancelAnimationFrame(rafTimer);
rafTimer = requestAnimationFrame(toggleHeaderFloating);
});
If you need to support IE9 and below (which for this, I'd recommend just not having the sticky header at all), you can use Paul's polyfill for rAF from 2011.
jQuery selecting
The original code does two things that bothers me:
- Runs a jQuery selector every time the scroll event fires
- Queries every element
Admittedly getElementsByClassName
(which jQuery/sizzle uses if the selector is a single class) is pretty well optimised, so it's not of great concern. However, we don't need to construct a new jQuery object on every scroll tick.
For an idea of the number of times that code would be run, in Chrome devtools put this the console and scroll the page:
window.onscroll = () => console.count('scroll')
// or monitorEvents('scroll')
Let's cache:
var $headerSection = $('.header-section');
var toggleHeaderFloating = function() {
// Floating Header
if ( $window.scrollTop() > 80 ) {
$headerSection.addClass( 'floating' );
} else {
$headerSection.removeClass( 'floating' );
};
};
var rafTimer;
$window.on('scroll', function () {
cancelAnimationFrame(rafTimer);
rafTimer = requestAnimationFrame(toggleHeaderFloating);
});
Only change the class once
The class on the header section really only needs changing in one scenario: when the scroll position goes over a certain threshold.
The alternative is to use classList
as it's optimised to check whether the class needs changing before it touches the DOM.
Here's my (vanilla) version that I use on the ffconf 2016 conference site:
var rafTimer;
window.onscroll = function (event) {
cancelAnimationFrame(rafTimer);
rafTimer = requestAnimationFrame(toggleHeaderFloating);
};
function toggleHeaderFloating() {
// does cause layout/reflow: https://git.io/vQCMn
if (window.scrollY > 80) {
document.body.classList.add('sticky');
} else {
document.body.classList.remove('sticky');
}
}
In the next part, I'll share how I combined this technique with smooth scrolling.