Пастка циклу подій у JavaScript: складність таймерів і RAF

привіт, розробники! Сьогодні я поділюсь поширеною, але рідко правильно вирішеною проблемою в JavaScript: проблемами синхронізації при використанні setTimeout/setInterval та requestAnimationFrame в анімаціях.

pic

Сценарій проблеми

Уявіть, що ви розробляєте функцію плавного прокручування для веб-додатку. Коли користувач натискає кнопку, сторінка повинна плавно прокручуватися до певної точки. Хоча на перший погляд це здається простим, в цій задачі є кілька підводних каменів.

pic

Більшість розробників вирішить це так:

function smoothScroll(targetPosition, duration) {  
 const startPosition = window.pageYOffset;  
 const distance = targetPosition - startPosition;  
 let startTime = null;
function scrollAnimation(currentTime) {  
 if (startTime === null) startTime = currentTime;  
 const timeElapsed = currentTime - startTime;  
 const progress = Math.min(timeElapsed / duration, 1);  

 window.scrollTo(0, startPosition + distance * progress);  

 if (timeElapsed < duration) {  
 setTimeout(() => {  
 scrollAnimation(performance.now());  
 }, 16); // приблизно 60fps  
 }  
 }  
 scrollAnimation(performance.now());  
}  
// Використання  
document.getElementById('scrollButton').onclick = () => {  
 smoothScroll(1000, 1000); // Прокрутити до 1000px за 1 секунду  
};

Хоча цей код виглядає працюючим, він має серйозні недоліки:

  1. Пропущені кадри (jank)
  2. Нерегулярна швидкість анімації
  3. Високе навантаження на процесор
  4. Споживання батареї

Технічне пояснення

У JavaScript setTimeout/setInterval не синхронізовані з циклом рендерингу браузера. Це означає:

  1. Деякі кадри можуть бути пропущені
  2. Деякі кадри можуть бути оброблені двічі
    3.
    Розрив екрана може виникнути через відсутність синхронізації з V-Sync

pic

Правильне рішення

Ось правильна реалізація тієї ж функціональності:

function smoothScrollRAF(targetPosition, duration) {  
 const startPosition = window.pageYOffset;  
 const distance = targetPosition - startPosition;  
 let startTime = null;  
 let previousTimeStamp = null;  
 let done = false;
function easeOutCubic(t) {  
 return 1 - Math.pow(1 - t, 3);  
 }  
 function scrollStep(currentTime) {  
 if (startTime === null) {  
 startTime = currentTime;  
 }  
 const timeElapsed = currentTime - startTime;  
 const progress = Math.min(timeElapsed / duration, 1);  

 // Функція для плавного руху  
 const easedProgress = easeOutCubic(progress);  

 // Перевірка часу кадру  
 if (previousTimeStamp !== currentTime) {  
 window.scrollTo(0, startPosition + distance * easedProgress);  
 }  

 previousTimeStamp = currentTime;  
 // Анімація не завершена, кадр не скасовано  
 if (progress < 1 && !done) {  
 requestAnimationFrame(scrollStep);  
 }  
 }  
 // Початок анімації  
 const animationFrame = requestAnimationFrame(scrollStep);  
 // Функція для очищення  
 return function cleanup() {  
 done = true;  
 cancelAnimationFrame(animationFrame);  
 };  
}  
// Використання та очищення  
let currentScrollAnimation = null;  
document.getElementById('scrollButton').onclick = () => {  
 // Очищення попередньої анімації  
 if (currentScrollAnimation) {  
 currentScrollAnimation();  
 }  

 currentScrollAnimation = smoothScrollRAF(1000, 1000);  
};  
// Очищення при зміні сторінки  
window.onunload = () => {  
 if (currentScrollAnimation) {  
 currentScrollAnimation();  
 }  
};

Чому це рішення краще

Використання requestAnimationFrame

  • Синхронізується з циклом рендерингу браузера
  • Сумісно з V-Sync
  • Автоматично призупиняється у фонових вкладках

Перевірка часу кадру

  • Запобігає повторній обробці одного й того ж кадру
  • Оптимізує використання процесора

Механізм очищення

  • Запобігає витокам пам’яті
  • Уникає конфліктуючих анімацій

Функція для плавного руху

  • Забезпечує більш природний і плавний рух

Порівняння продуктивності

У нашому тестовому сценарії:

  • версія setTimeout: ~25% використання процесора
  • версія requestAnimationFrame: ~8% використання процесора

Кращі практики

  1. Завжди використовуйте requestAnimationFrame для анімацій
  2. Не забувайте про перевірки часу кадру
  3. Включайте механізми очищення
  4. Використовуйте функції для плавного руху
  5. Використовуйте Performance.now() для точнішого вимірювання часу

Поради для налагодження

Для виявлення проблем з продуктивністю в Chrome DevTools:

  1. Відкрийте вкладку Performance
  2. Виберіть вигляд “Frame” в Timeline
  3. Перевірте на пропущені кадри під час анімації
  4. Моніторте використання процесора

pic

Висновок

Використання requestAnimationFrame замість setTimeout/setInterval для анімацій у JavaScript має значний вплив на продуктивність та користувацький досвід. Цей підхід забезпечує:

  • Плавніші анімації
  • Нижче використання процесора
  • Кращу тривалість роботи батареї
  • Покращений користувацький досвід

Сподіваюся, ця стаття допоможе покращити ваш підхід до анімацій і синхронізації часу в JavaScript. Не соромтеся залишати свої запитання в коментарях!

Перекладено з: Event Loop Trap in JavaScript: The Complexity of Timers and RAF

Leave a Reply

Your email address will not be published. Required fields are marked *