Визуальные обновления в браузере подчиняются строгому циклу
перерисовки, где ключевую роль играет синхронизация вычислений с кадрами
отрисовки. Основной механизм, обеспечивающий стабильную частоту
обновлений без перегрузки главного потока, —
requestAnimationFrame.
Он выравнивает выполнение анимационных шагов с частотой обновления
экрана, обычно 60 FPS, и предотвращает избыточные вызовы рендера,
характерные для setInterval и setTimeout.
Функция requestAnimationFrame принимает callback,
который вызывается перед следующим перерисовыванием экрана. Это
позволяет строить предсказуемый и производительный цикл анимации.
Базовая структура:
function tick(timestamp) {
// вычисления состояния сцены
update(timestamp);
// отрисовка
render();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
Ключевая особенность — браузер сам регулирует частоту вызовов. При неактивной вкладке цикл замедляется, что снижает потребление ресурсов.
D3 предоставляет собственные инструменты управления временем и анимациями, среди которых:
d3.transition()d3.timer()d3.interpolate()Однако при сложной визуализации с Canvas или гибридной архитектуре
часто требуется ручной контроль через
requestAnimationFrame.
d3.timer является обёрткой над
requestAnimationFrame, добавляющей удобство управления
временем:
import { timer } from "d3-timer";
timer((elapsed) => {
update(elapsed);
render();
});
d3.transition() оптимизирован для DOM/SVG и управляет
интерполяцией свойств автоматически:
d3.select("circle")
.transition()
.duration(1000)
.attr("r", 50);
Но при высокочастотной анимации (частицы, физические симуляции, графы с тысячами узлов) DOM становится узким местом:
attr вызывает перерасчёт layoutВ таких случаях Canvas + RAF становится предпочтительным решением.
Canvas работает по принципу растровой перерисовки: каждый кадр полностью перезаписывает содержимое области.
Базовый цикл:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
function renderFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawScene(ctx);
requestAnimationFrame(renderFrame);
}
requestAnimationFrame(renderFrame);
Преимущество подхода — отсутствие затрат на управление DOM-деревом.
Производительные анимации разделяют:
D3-сила (force simulation) часто используется именно так:
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", render);
При Canvas-рендеринге вместо обновления DOM выполняется перерисовка сцены:
function render() {
ctx.clearRect(0, 0, width, height);
for (const node of nodes) {
ctx.beginPath();
ctx.arc(node.x, node.y, 3, 0, Math.PI * 2);
ctx.fill();
}
}
d3.timer позволяет строить анимации без явного вызова
requestAnimationFrame, сохраняя контроль над временем:
import { timer } from "d3-timer";
timer((elapsed) => {
const t = elapsed / 1000;
nodes.forEach(n => {
n.x += Math.cos(t + n.id);
n.y += Math.sin(t + n.id);
});
render();
});
Параметр elapsed обеспечивает стабильную временную
шкалу, что важно для детерминированных симуляций.
D3 использует систему интерполяции значений через
d3-interpolate. Это позволяет плавно переходить между
состояниями без ручного вычисления шагов.
Пример линейной интерполяции:
const i = d3.interpolateNumber(0, 100);
console.log(i(0.5)); // 50
В RAF-цикле интерполяция часто применяется для управления анимацией вручную:
const start = performance.now();
const duration = 1000;
function animate(now) {
const t = Math.min(1, (now - start) / duration);
const value = d3.interpolateNumber(0, 200)(t);
ctx.clearRect(0, 0, width, height);
ctx.fillRect(10, 10, value, 20);
if (t < 1) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
SVG-подход удобен, но имеет ограничения при масштабировании:
Canvas лишён этих проблем, но требует ручного управления сценой.
Ключевая оптимизация в RAF-циклах — объединение операций отрисовки:
Плохой подход:
nodes.forEach(drawNode); // может вызывать лишние состояния ctx
Оптимизированный подход:
ctx.beginPath();
for (const node of nodes) {
ctx.moveTo(node.x + 2, node.y);
ctx.arc(node.x, node.y, 2, 0, Math.PI * 2);
}
ctx.fill();
Снижение количества вызовов fill, stroke и
beginPath напрямую влияет на производительность.
Производительные системы строятся по схеме:
function update() {
for (const node of nodes) {
node.vx *= 0.98;
node.vy *= 0.98;
node.x += node.vx;
node.y += node.vy;
}
}
function render() {
ctx.clearRect(0, 0, width, height);
for (const node of nodes) {
ctx.fillRect(node.x, node.y, 2, 2);
}
}
При потоковых данных (например, телеметрия или симуляции) частота обновления может превышать 60 Hz. В таких случаях используется throttling внутри RAF:
let lastRender = 0;
const frameInterval = 1000 / 60;
function loop(now) {
if (now - lastRender > frameInterval) {
render();
lastRender = now;
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
D3-scale остаются полезными даже в Canvas-рендеринге:
const x = d3.scaleLinear()
.domain([0, 100])
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, 100])
.range([height, 0]);
function render() {
ctx.clearRect(0, 0, width, height);
for (const d of data) {
ctx.fillRect(x(d.x), y(d.y), 3, 3);
}
}
Разделение логики масштабирования и отрисовки упрощает поддержку и повышает читаемость вычислительного слоя.
Частые ошибки производительных анимаций:
Паттерн с переиспользованием объектов:
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
n.x += n.vx;
n.y += n.vy;
}
Избегание Array.map и filter внутри RAF
снижает давление на сборщик мусора.
В сложных визуализациях часто используется гибрид:
Это позволяет сохранить интерактивность без потери производительности.
Критически важный аспект — использование timestamp, а не
фиксированных шагов:
let last = performance.now();
function loop(now) {
const dt = now - last;
last = now;
update(dt);
render();
requestAnimationFrame(loop);
}
Такой подход делает симуляции независимыми от FPS и аппаратной нагрузки.