---
date: 2026-05-17
type: ERR
scope: canvas animation / requestAnimationFrame
status: 已修复
---

# Canvas 动画双 RAF 调度 → 动画"越来越快"

## 现象

用户报告"页面动画越跑越快，越闪得越快"。具体表现：神经元背景的信号脉冲传导速度随时间加快，特别是**切 tab 离开再回来后越来越明显**。

## 根因（双层 RAF 调度）

写背景 canvas 动画时同时用了两套调度：

```js
function tick(){
  // ... draw and update ...
  requestAnimationFrame(tick);  // ← 内层自己 RAF 自己
}

let raf = null;
function start(){
  if (!raf) raf = requestAnimationFrame(function loop(){
    tick();                              // ← 外层 loop 也调 tick
    raf = requestAnimationFrame(loop);
  });
}
function stop(){ if (raf) { cancelAnimationFrame(raf); raf = null; } }

document.addEventListener('visibilitychange', () => document.hidden ? stop() : start());
start();
```

**两个问题串联**：

1. **第一次 start()** 就建立了 2 条独立的 RAF 链：
   - 外层 loop → tick() → next RAF
   - tick() 内部又自己 → next RAF
   - 每帧 `tick()` 被调 2 次

2. **visibilitychange 切 tab 时**：
   - `stop()` 只 cancel 了 `raf`（外层 loop 链）
   - tick 内部的 self-RAF 链**没被 cancel**，继续在跑
   - 切回来 `start()` 又新开一条 → 总共 **3 条链**
   - 再切一次 tab 累积成 5 条、7 条...

切的次数越多，链越多，动画看起来越快。

## 修复

**只保留一条 RAF 链**，tick 不再自己调度：

```js
function tick(){
  // ... draw and update ...
  // 不再 requestAnimationFrame(tick)
}

let raf = null;
function loop(){ tick(); raf = requestAnimationFrame(loop); }
function start(){ if (raf == null) raf = requestAnimationFrame(loop); }
function stop(){ if (raf != null) { cancelAnimationFrame(raf); raf = null; } }
document.addEventListener('visibilitychange', () => document.hidden ? stop() : start());
start();
```

`tick()` 只负责一帧的绘制 + 状态更新；调度全部由 `loop()` 管理，stop/start 真正切换。

## 适用范围

任何 canvas / requestAnimationFrame 长跑动画都要看一眼：
- 是不是函数自己在 self-RAF？
- 是不是同时被外层 loop 调度？
- 两个都做会重复调度

## 检测/避免方法

- **rule of thumb**：写 RAF 时确认"只有一处地方在 `requestAnimationFrame(...)`"
- 如果用 `visibilitychange` 暂停/恢复，必须保证 stop() 能真的彻底停下所有 RAF 链
- 调试时：在 tick 内 `console.count('tick')` 看每秒被调多少次，如果远超 60 就是有多重调度

## 关联

- 文件：`/home/chentao/Code/ffworkspace-wt/.agent-pool/slot-5/frontend/public/showcase-pages/informatization-overview.html` 神经元背景动画
