必须用requestAnimationFrame驱动循环,粒子是内存对象数组而非DOM节点,每次渲染清空画布重绘全部;setInterval无法对齐屏幕刷新率,易掉帧、卡顿、瞬移或拖影,而requestAnimationFrame天然同步VSync且页面不可见时自动暂停。

必须用 requestAnimationFrame 驱动循环,粒子是内存对象数组而非 DOM 节点,每次渲染都要清空画布重绘全部——这是唯一能保证流畅、不拖影的实现路径。
为什么不能用 setInterval 更新 Canvas 粒子
掉帧、卡顿、时间不准是硬伤。setInterval 无法对齐屏幕刷新率,尤其在页面后台或低性能设备上,容易累积延迟,导致粒子“瞬移”或堆叠拖影。而 requestAnimationFrame 由浏览器调度,天然同步垂直同步(VSync),且会在页面不可见时自动暂停。
- 别写
setInterval(update, 16)—— 它不是 60fps 的等价替代 - 如果非要兼容旧环境(如 IE9),需 fallback 到
setTimeout+ 时间戳差值校正,但现代项目基本不用考虑 - 动画循环里别混用
setTimeout或Promise.then做帧控制,会破坏节奏
粒子对象怎么设计才不卡
轻量、扁平、避免 getter/setter 和原型链深拷贝。每个粒子只需存最必要的状态:位置、速度、尺寸、透明度、颜色(或索引)。不要塞进方法,更新逻辑统一由系统函数处理。
- 推荐结构:
{x: 0, y: 0, vx: 0.5, vy: -0.2, size: 2, alpha: 1, color: '#ff6b6b'} - 颜色尽量复用字符串或预设数组,避免每帧 new Color() 或调用
ctx.fillStyle = `rgba(...)` - 粒子数超 300 时,考虑分组绘制(同色 batch)、禁用
shadowBlur、关闭抗锯齿(ctx.imageSmoothingEnabled = false) - 别在
draw()里反复调用ctx.beginPath()和ctx.closePath(),圆点直接用arc()+fill()即可
鼠标交互时粒子抖动或穿出边界怎么办
本质是边界检测和位置校正没做闭环。只反转速度(vx = -vx)但不把坐标拉回安全区,会导致下一帧立刻再次触发碰撞判断,形成高频震荡。
立即学习“前端免费学习笔记(深入)”;
- 反弹逻辑必须包含两步:① 反转速度;② 将坐标强制设为边缘内侧(如
x = canvas.width - 1) - 鼠标影响范围建议用距离平方比较(
(mx - x) ** 2 + (my - y) ** 2 ),比开方快 3 倍 - 鼠标坐标要按 canvas 实际像素尺寸缩放:
const rect = canvas.getBoundingClientRect(); const mx = (e.clientX - rect.left) * (canvas.width / rect.width); - 粒子被鼠标推开后,若不做阻尼(
vx *= 0.98),会永远飞走;若阻尼过强,又显得黏滞——0.95~0.98 是较自然区间
动画跑久了内存暴涨或变慢
透明粒子(alpha ≤ 0.01)没被及时剔除,持续参与计算和绘制,是常见泄漏源。Canvas 不会自动回收“看不见”的粒子。
- 每帧检查
particle.alpha ,满足则用 <code>splice(i, 1)或标记 + 后续批量清理 - 避免用
filter()每帧重建数组(GC 压力大),改用双指针原地覆盖或池化复用 - 全屏 canvas 的
width/height必须显式设为像素值(canvas.width = window.innerWidth),不能只靠 CSS 缩放,否则实际渲染分辨率爆炸,GPU 崩溃 - resize 事件里别直接重置 canvas 尺寸,先
cancelAnimationFrame,再清空数组,最后重启循环
真正难的不是画出第一个粒子,而是让上千个粒子在各种屏幕、各种交互下都稳定跑满 60fps——关键在清空时机、坐标校正、内存清理这三处,漏掉任何一环,效果就从酷炫变成卡顿。



















