Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «requestAnimationFrame та requestIdleCallback в JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**requestAnimationFrame** планує callback перед наступним перемалюванням браузера, синхронно з частотою оновлення дисплею (~60fps). **requestIdleCallback** виконується під час вільного часу, після обробки вводу та рендерингу. ```js // RAF: покадрова анімація function animate(time) { box.style.transform = `translateX(${time * 0.05}px)`; requestAnimationFrame(animate); } requestAnimationFrame(animate); // RIC: фонова робота з бюджетом idle-часу requestIdleCallback((deadline) => { while (deadline.timeRemaining() > 0) doTask(); }, { timeout: 200 }); ``` **Головне:** RAF = термінові візуальні оновлення. RIC = відкладені фонові задачі.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**requestAnimationFrame** планує виклик callback-функції прямо перед тим, як браузер перемалює наступний кадр. **requestIdleCallback** відкладає роботу до вільного часу, після того як завершено обробку вводу та рендеринг. ## Теорія ### TL;DR - RAF спрацьовує ~60 разів на секунду, синхронізовано з частотою оновлення дисплею (vsync). RIC спрацьовує нерегулярно, тільки коли браузеру більше нічим зайнятись. - Аналогія: RAF - це художник, який каже "пензель готовий?" перед кожним мазком. RIC - прибиральник, який прибирає тільки після того, як художник закінчив всю кімнату. - Головна різниця: RAF прив'язаний до циклу перемалювання (термінова візуалізація); RIC чекає після обробки вводу та layout (фонові задачі). - Анімація або оновлення UI? Використовуй RAF. Аналітика, логування або prefetch? RIC. - RIC не підтримується в Safari, тому завжди додавай `{ timeout: ms }` як запасний варіант. ### Короткий приклад ```js const box = document.querySelector('#box'); // RAF: спрацьовує перед кожним перемалюванням, ~60fps function animate(time) { box.style.transform = `translateX(${time * 0.05}px)`; requestAnimationFrame(animate); // плануємо наступний кадр } requestAnimationFrame(animate); // RIC: спрацьовує тільки під час вільних слотів function doIdleWork(deadline) { while (deadline.timeRemaining() > 0) { console.log('фонова задача'); // поважає доступний бюджет часу } requestIdleCallback(doIdleWork, { timeout: 200 }); // повторити, з fallback по таймауту } requestIdleCallback(doIdleWork, { timeout: 200 }); ``` RAF передає у callback мітку часу `performance.now()` з високою точністю. RIC передає об'єкт `deadline`, щоб код міг перевірити, скільки вільного часу ще залишилось перед тим, як браузер забере потік назад. ### Ключова різниця Колбеки RAF запускаються під час фази "begin frame" в браузері, до перерахунку стилів, layout, paint і compositing. Вони синхронізовані з vsync-сигналом від GPU, зазвичай з інтервалом 16.7мс на 60Hz дисплеї. Колбеки RIC потрапляють у планувальник задач браузера після всього іншого і виконуються тільки в залишковому часовому слоті після повного рендерингу кадру. Цей слот зазвичай 4-10мс, а на завантаженій сторінці може бути і нульовим. ### Коли що використовувати - DOM-анімації, canvas або все, що має залишатись плавним на 60fps: RAF. - Реакція на scroll чи resize, якщо зачіпається layout: RAF. - Аналітика та telemetry після гідратації: RIC. - Фонові черги синхронізації або ненав'язливий prefetch: RIC. - Вкладка згорнута: RAF автоматично призупиняється, RIC теж сповільнюється. - Мобільні пристрої або режим економії батареї: RIC з `{ timeout: 500 }`, щоб callback точно не пропустили. ### Таблиця порівняння | Параметр | requestAnimationFrame | requestIdleCallback | |---|---|---| | Тригер | Перед наступним перемалюванням (~16мс на 60Hz) | Після задач кадру (змінний вільний слот) | | Пріоритет | Високий (візуальні оновлення) | Низький (фон) | | Параметри callback | `callback(timestamp)` | `callback(deadline, options)` | | Скасування | `cancelAnimationFrame(id)` | `cancelIdleCallback(id)` | | Підтримка браузерів | Всі сучасні, IE10+ | Chrome/Edge 60+, Firefox 87+. Safari не підтримує. | | Запасний варіант | Немає | `{ timeout: ms }` запустить callback після затримки | | Типове застосування | Render loop Three.js, частинки Phaser.js | Аналітика Vercel, черги Workbox | ### Як браузер це обробляє У Chromium колбеки RAF ставляться в чергу на головному потоці й запускаються під час події "begin frame" перед растеризацією, синхронно з vsync GPU. Тому вони залишаються плавними і на 120Hz: браузер просто викликає їх удвічі частіше. RIC управляється через `IdleTaskController`, який вимірює час, що залишився після стилів, layout і compositing, та виділяє залишок через `deadline.timeRemaining()`. Якщо сторінка ніколи не простоює (активна реклама, ігровий цикл), колбеки RIC можуть взагалі не запуститись без параметра `timeout`. ### Типові помилки **Помилка: `setInterval` всередині RAF-циклу** ```js // Неправильно: дві окремі черги, призводить до пропущених кадрів function badAnimate() { setInterval(() => { box.style.left = x++ + 'px'; }, 16); requestAnimationFrame(badAnimate); } ``` Інтервали виконуються в окремій черзі таймерів і не синхронізовані з vsync. Отримуєш подвійне планування і пропущені кадри. Використовуй тільки рекурсивний RAF. **Помилка: ігнорування `deadline.timeRemaining()` в RIC** ```js // Неправильно: може виконуватись 100мс, блокує наступний paint requestIdleCallback(() => { heavyComputation(); }); // Правильно: розбиваємо роботу на частини requestIdleCallback((deadline) => { while (deadline.timeRemaining() > 1 && tasks.length) { processOneTask(tasks.shift()); } }); ``` Якщо перевищити вільний слот, наступна реакція на ввід або перемалювання відкладаються. Розбивай роботу на частини, що вкладаються в доступний бюджет. **Помилка: немає скасування при знищенні компонента** ```js // Неправильно: id загублено, цикл не зупиняється useEffect(() => { requestAnimationFrame(animate); }, []); // Правильно: зберігаємо id, скасовуємо в cleanup useEffect(() => { const id = requestAnimationFrame(animate); return () => cancelAnimationFrame(id); }, []); ``` "Привид" RAF-циклу продовжує з'їдати CPU після того, як компонент видалено з DOM. Завжди зберігай id і скасовуй у функції cleanup. **Помилка: RIC без timeout на мобільних** Режим економії батареї та фонові вкладки можуть ніколи не дати вільного вікна. `{ timeout: 500 }` вказує браузеру запустити callback через 500мс у будь-якому випадку. Без цього аналітичний код може зависнути на мобільних непомітно. ### Де зустрічається в реальних проєктах - Three.js: `renderer.setAnimationLoop` - тонка обгортка над RAF. - React Reanimated: `withSpring` і драйвери жестів використовують RAF-цикли для покадрових оновлень. - Preact Signals: RAF групує DOM-коміти, щоб уникнути кількох перемалювань за один тік. - Vercel Analytics: RIC відкладає пінги pageview до завершення гідратації. - Workbox (PWA-бібліотека від Google): RIC керує чергами фонової синхронізації між запитами. ### Питання на співбесіді **Q:** Навіщо RAF передає timestamp у callback? **A:** Це значення `performance.now()` з високою точністю. Воно дозволяє вирахувати `delta = time - lastTime` і зробити анімацію незалежною від частоти кадрів. **Q:** Що відбувається з RAF, коли вкладка згорнута? **A:** Браузер призупиняє або сповільнює його до приблизно 1fps, економлячи CPU і батарею. Звичайний `setTimeout` продовжував би виконуватись з повною частотою. **Q:** Що таке об'єкт `deadline` у RIC? **A:** Він має два члени: `timeRemaining()` повертає скільки мілісекунд залишилось у поточному вільному слоті, а `didTimeout` дорівнює `true`, якщо параметр `timeout` примусив запуск callback до появи вільного вікна. **Q:** Як зробити polyfill для RIC в Safari? **A:** Найпростіший варіант - обгорнути `setTimeout` з коротким затриманням (0-1мс). Точніший перевіряє `performance.now()` відносно останнього відомого зайнятого часу і відкладає відповідно. **Q:** Senior-питання: на 120Hz дисплеї з важким JS, як RAF взаємодіє з compositor thread? **A:** Колбеки RAF виконуються на головному потоці до стилів і layout. Але якщо застосувати `will-change: transform` або `transform3d` до елемента, браузер переносить його на compositor layer. Анімації на цьому шарі обробляються поза головним потоком, тому залишаються плавними навіть коли JS блокує. Відповідь рівня senior: "переноси на compositor layer через `will-change`, щоб малювання обійшло вузьке місце головного потоку." ## Приклади ### Плавна анімація лічильника в React ```js import { useRef, useEffect } from 'react'; function AnimatedCounter({ target }) { const countRef = useRef(0); useEffect(() => { let rafId; const animate = (time) => { // Плавно наближаємось до target: 5% відстані за кожен кадр countRef.current += (target - countRef.current) * 0.05; document.getElementById('counter').textContent = Math.round(countRef.current); if (Math.abs(target - countRef.current) > 0.5) { rafId = requestAnimationFrame(animate); } }; rafId = requestAnimationFrame(animate); return () => cancelAnimationFrame(rafId); // скасовуємо при знищенні }, [target]); return <span id="counter">0</span>; } ``` Кожен RAF-виклик наближає лічильник на 5% до цільового значення. Анімація природно сповільнюється без CSS-переходів. Скасування при unmount запобігає "привидному" циклу після видалення компонента з DOM. ### Групування аналітики через RIC з timeout ```js const analyticsQueue = []; function flushAnalytics(deadline) { // Обробляємо події тільки поки є вільний час while (deadline.timeRemaining() > 1 && analyticsQueue.length > 0) { const event = analyticsQueue.shift(); sendToServer(event); // неблокуючий fetch } // Якщо черга не вичерпана, плануємо наступний вільний слот if (analyticsQueue.length > 0) { requestIdleCallback(flushAnalytics, { timeout: 300 }); } } function trackEvent(name, data) { analyticsQueue.push({ name, data, ts: performance.now() }); requestIdleCallback(flushAnalytics, { timeout: 300 }); } ``` `{ timeout: 300 }` гарантує очищення черги протягом 300мс навіть якщо сторінка залишається завантаженою. Я бачив цей підхід у production-дашбордах, де аналітика мовчки зникала на мобільних до того, як додали timeout. Без нього RIC-колбеки можуть накопичуватись нескінченно на важких сторінках. ### Ігровий цикл з delta-time (стиль Three.js) ```js const clock = { last: 0 }; function gameLoop(time) { const delta = time - clock.last; // мілісекунди з останнього кадру clock.last = time; updatePhysics(delta); // рухаємо об'єкти пропорційно до delta updateCamera(delta); renderer.render(scene, camera); requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); ``` Використання `delta` робить рух незалежним від частоти кадрів. На 60fps `delta` дорівнює ~16мс, на 120fps - ~8мс. Об'єкт проходить однакову відстань за секунду в обох випадках, що важливо коли у користувачів різне залізо.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.