привіт, розробники! Сьогодні я поділюсь поширеною, але рідко правильно вирішеною проблемою в JavaScript: проблемами синхронізації при використанні setTimeout
/setInterval
та requestAnimationFrame
в анімаціях.
Сценарій проблеми
Уявіть, що ви розробляєте функцію плавного прокручування для веб-додатку. Коли користувач натискає кнопку, сторінка повинна плавно прокручуватися до певної точки. Хоча на перший погляд це здається простим, в цій задачі є кілька підводних каменів.
Більшість розробників вирішить це так:
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 секунду
};
Хоча цей код виглядає працюючим, він має серйозні недоліки:
- Пропущені кадри (jank)
- Нерегулярна швидкість анімації
- Високе навантаження на процесор
- Споживання батареї
Технічне пояснення
У JavaScript setTimeout
/setInterval
не синхронізовані з циклом рендерингу браузера. Це означає:
- Деякі кадри можуть бути пропущені
- Деякі кадри можуть бути оброблені двічі
3.
Розрив екрана може виникнути через відсутність синхронізації з V-Sync
Правильне рішення
Ось правильна реалізація тієї ж функціональності:
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% використання процесора
Кращі практики
- Завжди використовуйте
requestAnimationFrame
для анімацій - Не забувайте про перевірки часу кадру
- Включайте механізми очищення
- Використовуйте функції для плавного руху
- Використовуйте Performance.now() для точнішого вимірювання часу
Поради для налагодження
Для виявлення проблем з продуктивністю в Chrome DevTools:
- Відкрийте вкладку Performance
- Виберіть вигляд “Frame” в Timeline
- Перевірте на пропущені кадри під час анімації
- Моніторте використання процесора
Висновок
Використання requestAnimationFrame
замість setTimeout
/setInterval
для анімацій у JavaScript має значний вплив на продуктивність та користувацький досвід. Цей підхід забезпечує:
- Плавніші анімації
- Нижче використання процесора
- Кращу тривалість роботи батареї
- Покращений користувацький досвід
Сподіваюся, ця стаття допоможе покращити ваш підхід до анімацій і синхронізації часу в JavaScript. Не соромтеся залишати свої запитання в коментарях!
Перекладено з: Event Loop Trap in JavaScript: The Complexity of Timers and RAF