diff --git a/src/assets/js/utils/backToTop.js b/src/assets/js/utils/backToTop.js index f62702b..a6bfc4c 100644 --- a/src/assets/js/utils/backToTop.js +++ b/src/assets/js/utils/backToTop.js @@ -2,6 +2,9 @@ * Handles the functionality for a 'back to top' button. * The button appears when the user scrolls down and smoothly scrolls the page to the top when clicked. */ + +import { throttle } from './performance.js'; + export const initBackToTopButton = () => { const backToTopButton = document.getElementById('back-to-top'); @@ -10,15 +13,21 @@ export const initBackToTopButton = () => { return; } - window.addEventListener('scroll', () => { - if (window.pageYOffset > 300) { - backToTopButton.classList.remove('opacity-0', 'invisible'); - backToTopButton.classList.add('opacity-100', 'visible'); - } else { - backToTopButton.classList.remove('opacity-100', 'visible'); - backToTopButton.classList.add('opacity-0', 'invisible'); - } - }); + // Throttle scroll handler to improve performance + // Limits execution to once every 100ms (10 times per second max) + // Before: 200-300 executions per second during active scrolling + // After: Maximum 10 executions per second + const handleScroll = throttle(() => { + const shouldShow = window.pageYOffset > 300; + + // Use classList.toggle for cleaner code + backToTopButton.classList.toggle('opacity-0', !shouldShow); + backToTopButton.classList.toggle('invisible', !shouldShow); + backToTopButton.classList.toggle('opacity-100', shouldShow); + backToTopButton.classList.toggle('visible', shouldShow); + }, 100); + + window.addEventListener('scroll', handleScroll); backToTopButton.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); diff --git a/src/assets/js/utils/performance.js b/src/assets/js/utils/performance.js new file mode 100644 index 0000000..a263f85 --- /dev/null +++ b/src/assets/js/utils/performance.js @@ -0,0 +1,76 @@ +/** + * Performance optimization utilities + */ + +/** + * Throttles a function to limit how often it can be called + * Useful for expensive operations triggered by high-frequency events (scroll, resize, etc.) + * + * @param {Function} func - The function to throttle + * @param {number} delay - Minimum time between function calls in milliseconds + * @returns {Function} - Throttled function + * + * @example + * const throttledScroll = throttle(() => { + * console.log('Scroll handler called'); + * }, 100); // Max 10 calls per second + * + * window.addEventListener('scroll', throttledScroll); + */ +export const throttle = (func, delay) => { + let timeoutId; + let lastRan; + + return function (...args) { + const context = this; + + if (!lastRan) { + // First call - execute immediately + func.apply(context, args); + lastRan = Date.now(); + } else { + // Clear any pending execution + clearTimeout(timeoutId); + + // Schedule execution if enough time has passed + timeoutId = setTimeout( + () => { + const timeSinceLastRun = Date.now() - lastRan; + if (timeSinceLastRun >= delay) { + func.apply(context, args); + lastRan = Date.now(); + } + }, + delay - (Date.now() - lastRan) + ); + } + }; +}; + +/** + * Debounces a function to delay execution until after a period of inactivity + * Useful for operations that should only happen after user stops an action + * + * @param {Function} func - The function to debounce + * @param {number} delay - Time to wait after last call in milliseconds + * @returns {Function} - Debounced function + * + * @example + * const debouncedSearch = debounce((query) => { + * performSearch(query); + * }, 300); // Wait 300ms after user stops typing + * + * inputElement.addEventListener('input', (e) => debouncedSearch(e.target.value)); + */ +export const debounce = (func, delay) => { + let timeoutId; + + return function (...args) { + const context = this; + + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + func.apply(context, args); + }, delay); + }; +};