I'm utilizing Intersection Observer API to hide the website header logo when it's not over (or close to being over) a background image.
Works well on Google Chrome, other chromium browsers and Safari (at the least mobile version) but the CSS animations play extremely laggy and sometimes delayed on Firefox. In fact sometimes it doesn't even trigger, it just freezes.
The CSS animations display fine when it's not triggered by Intersection Observer adding a class. (i.e.: the blue box in the example below has the fade-in class by default and the animation plays smoothly when you refresh the page and it fades in.)
I'm using Firefox 132.0.1 (64-bit). I've asked others to test the website and the results were the same across.
Here is a sample page I made replicating the issue with a blue square as the header logo and red rectangle as the background image region:
const header = document.getElementById("headerhide");
const region = document.querySelector(".header-background");
const observer = new IntersectionObserver(([entry]) => {
header.classList.toggle("fade-out", !entry.isIntersecting);
header.classList.toggle("fade-in", entry.isIntersecting);
}, {
threshold: 0.2,
})
observer.observe(region)
.header-background {
height: 500px;
background-color: red;
}
.logo-container {
position: fixed;
top: 2%;
left: 50%;
transform: translate(-50%, 0);
width: 80px;
height: 80px;
background-color: blue;
}
.logo.logo-container {
position: absolute;
}
.container {
height: 2000px;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.fade-in {
opacity: 0;
animation-name: fade-in;
animation-duration: 0.4s;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
}
.fade-out {
opacity: 1;
animation-name: fade-out;
animation-duration: 0.2s;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
}
<div class="container">
<div class="headerhide" id="headerhide">
<div class="logo-container">
<div class="logo"></div>
</div>
</div>
<div class="header-background" id="region"></div>
</div>
(edit: also on JSFiddle: https://jsfiddle.net/ob2t6wn3/3/)
(the CSS and js is normally in different files but for the example's sake I put them all in the html)
My specs (although I've tested on different devices and the issue persisted.)
OS: Windows 11 Home [64-bit] CPU: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz GPU: NVIDIA GeForce RTX 2070 Memory: 16GB Motherboard: Micro-Star International Co., Ltd. MS-
Things I've tried:
Checking if it's a Firefox settings issue, I ruled out the privacy.fingerprintingProtection flag in about:config and Firefox Sync because disabling either or both did not solve the issue. I couldn't think of any other setting that could be causing this.
Adding will-change: opacity;
to the CSS class, didn't work.
Adding a slight transform animation to trigger hardware acceleration but didn't work, adding a translateZ(0)
also didn't work.
Adding debounce to the JavaScript function didn't work (although I'm not entirely sure if I did it correctly, I've started learning JavaScript 3 days ago.)
Using 2 observers to toggle the different classes, didn't work.
Trying on a fresh Firefox install (Developer Edition).
I tried using CSS transitions instead of keyframes but that didn't fix it either.
I've managed to make it work on my end using the code below.
I remove the positioning-values (top
, position: fixed
etc.) and applied them to the element being observed instead - #headerhide
.
I cannot give you a thorough explanation as to why this works, but I'm assuming it's either because #headerhide
before served as an empty container with a height of 0, because its only child element had position: fixed
, or because the animation now runs directly on the element that is visible.
But the above is just guesswork.
const header = document.getElementById("headerhide");
const region = document.querySelector(".header-background");
const observer = new IntersectionObserver(([entry]) => {
header.classList.toggle("fade-out", !entry.isIntersecting);
header.classList.toggle("fade-in", entry.isIntersecting);
}, {
threshold: 0.2,
})
observer.observe(region)
.header-background {
height: 500px;
background-color: red;
}
#headerhide {
width: 80px;
height: 80px;
z-index: 100;
top: 2%;
left: 50%;
transform: translate(-50%, 0);
position: fixed;
}
.logo-container {
background-color: blue;
height: 100%;
width: 100%;
}
.logo.logo-container {
position: absolute;
}
.container {
height: 2000px;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.fade-in {
opacity: 0;
animation-name: fade-in;
animation-duration: 0.4s;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
}
.fade-out {
opacity: 1;
animation-name: fade-out;
animation-duration: 0.2s;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
}
<div class="container">
<div class="headerhide" id="headerhide">
<div class="logo-container">
<div class="logo"></div>
</div>
</div>
<div class="header-background" id="region"></div>
</div>