🤞Fingers Crossed

The Carousel That Never Sleeps

TL;DR: A carousel that runs infinite scroll listeners and DOM queries like it's training for a marathon.

#javascript#performance#requestAnimationFrame#scroll-events#intersection-observer

The Code

javascript
1let hero_slide = document.querySelector('#hero-slide')
2
3let hero_slide_items = hero_slide.querySelectorAll('.slide')
4
5let hero_slide_index = 0
6
7let hero_slide_play = true
8
9let hero_slide_control_items = hero_slide.querySelectorAll('.slide-control-item')
10
11let slide_next = hero_slide.querySelector('.slide-next')
12
13let slide_prev = hero_slide.querySelector('.slide-prev')
14
15let header = document.querySelector('header')
16
17showSlide = (index) => {
18    hero_slide.querySelector('.slide.active').classList.remove('active')
19    hero_slide.querySelector('.slide-control-item.active').classList.remove('active')
20    hero_slide_control_items[index].classList.add('active')
21    hero_slide_items[index].classList.add('active')
22}
23
24nextSlide = () => {
25    hero_slide_index = hero_slide_index + 1 === hero_slide_items.length ? 0 : hero_slide_index + 1
26    showSlide(hero_slide_index)
27}
28
29prevSlide = () => {
30    hero_slide_index = hero_slide_index - 1 < 0 ? hero_slide_items.length - 1 : hero_slide_index - 1
31    showSlide(hero_slide_index)
32}
33
34slide_next.addEventListener('click', () => nextSlide())
35
36slide_prev.addEventListener('click', () => prevSlide())
37
38// add event to slide select
39hero_slide_control_items.forEach((item, index) => {
40    item.addEventListener('click', () => showSlide(index))
41})
42
43// pause slide when mouse come in slider
44hero_slide.addEventListener('mouseover', () => hero_slide_play = false)
45
46// resume slide when mouse leave out slider
47hero_slide.addEventListener('mouseleave', () => hero_slide_play = true)
48
49setTimeout(() => hero_slide_items[0].classList.add('active'), 200);
50
51// auto slide
52// setInterval(() => {
53//     if (!hero_slide_play) return
54//     nextSlide()
55// }, 5000);
56
57// change header style when scroll
58window.addEventListener('scroll', () => {
59    if (document.body.scrollTop > 80 || document.documentElement.scrollTop > 80) {
60        header.classList.add('shrink')
61    } else {
62        header.classList.remove('shrink')
63    }
64})
65
66// element show on scroll
67
68let scroll = window.requestAnimationFrame || function(callback) {window.setTimeout(callback, 1000/60)}
69
70let el_to_show = document.querySelectorAll('.show-on-scroll')
71
72isElInViewPort = (el) => {
73    let rect = el.getBoundingClientRect()
74
75    let distance = 200
76
77    return (rect.top <= (window.innerHeight - distance || document.documentElement.clientHeight - distance))
78}
79
80loop = () => {
81    el_to_show.forEach(el => {
82        if (isElInViewPort(el)) el.classList.add('show')
83    })
84
85    scroll(loop)
86}
87
88loop()
89

The Prayer 🤞

🤞 Fingers crossed that users won't notice their laptop fans spinning up like jet engines when they visit our homepage! Maybe if we just ignore those performance warnings in DevTools, they'll go away on their own. I'm sure running querySelectorAll on every single scroll event is totally fine - computers are fast these days, right?

The Reality Check

This carousel is a performance nightmare masquerading as a simple slider. The requestAnimationFrame loop runs continuously, checking viewport positions on every frame even when nothing is happening. Combined with the scroll event listener that queries the DOM on every pixel scrolled, you've created a perfect storm of unnecessary computation that will drain mobile batteries faster than a teenager drains data.

The global variables scattered throughout create a maintenance headache, while the lack of error handling means any missing DOM element will crash the entire script. Users on slower devices will experience janky animations and delayed interactions, while the continuous RAF loop prevents the browser from properly optimizing performance during idle periods.

Search engines and accessibility tools will struggle with this implementation since there's no semantic structure or ARIA labels. The auto-advance feature is commented out, but even the manual controls lack proper keyboard navigation, making this carousel unusable for anyone not using a mouse.

The Fix

First, debounce that scroll event and cache your DOM queries. Create a proper class-based structure to encapsulate the carousel logic:

javascript
1class HeroCarousel {
2  constructor(selector) {
3    this.carousel = document.querySelector(selector);
4    if (!this.carousel) return;
5    
6    this.slides = this.carousel.querySelectorAll('.slide');
7    this.controls = this.carousel.querySelectorAll('.slide-control-item');
8    this.nextBtn = this.carousel.querySelector('.slide-next');
9    this.prevBtn = this.carousel.querySelector('.slide-prev');
10    this.currentIndex = 0;
11    this.isPlaying = true;
12    
13    this.init();
14  }
15  
16  init() {
17    this.bindEvents();
18    this.showSlide(0);
19  }
20  
21  showSlide(index) {
22    // Remove active classes
23    this.carousel.querySelector('.slide.active')?.classList.remove('active');
24    this.carousel.querySelector('.slide-control-item.active')?.classList.remove('active');
25    
26    // Add active classes
27    this.slides[index]?.classList.add('active');
28    this.controls[index]?.classList.add('active');
29    
30    this.currentIndex = index;
31  }
32}
33

For the scroll animations, use Intersection Observer instead of that resource-hungry RAF loop:

javascript
1class ScrollAnimations {
2  constructor() {
3    this.observer = new IntersectionObserver(
4      this.handleIntersection.bind(this),
5      { rootMargin: '-200px 0px' }
6    );
7    
8    document.querySelectorAll('.show-on-scroll')
9      .forEach(el => this.observer.observe(el));
10  }
11  
12  handleIntersection(entries) {
13    entries.forEach(entry => {
14      if (entry.isIntersecting) {
15        entry.target.classList.add('show');
16        this.observer.unobserve(entry.target); // Stop observing once shown
17      }
18    });
19  }
20}
21

Debounce the header scroll effect to prevent excessive DOM manipulation:

javascript
1const debounce = (func, wait) => {
2  let timeout;
3  return function executedFunction(...args) {
4    const later = () => {
5      clearTimeout(timeout);
6      func(...args);
7    };
8    clearTimeout(timeout);
9    timeout = setTimeout(later, wait);
10  };
11};
12
13const handleHeaderScroll = debounce(() => {
14  const header = document.querySelector('header');
15  const scrolled = window.pageYOffset > 80;
16  header?.classList.toggle('shrink', scrolled);
17}, 16); // ~60fps
18
19window.addEventListener('scroll', handleHeaderScroll, { passive: true });
20

Lesson Learned

Performance optimization isn't about making code work, it's about making it work efficiently for everyone.