JS Canvas动画在Firefox仅1FPS性能极低,Chromium正常求优化
Firefox下Canvas贝塞尔曲线动画帧率优化方案
我用2D Canvas实现了带动态背景的3条贝塞尔曲线旋转动画,在Chromium内核浏览器运行正常,但Firefox里帧率仅1FPS,尝试多种优化后仅提升至1.2FPS,无法满足需求。以下是针对性优化方案:
核心优化点
合并路径绘制,减少上下文切换
原代码每条贝塞尔曲线单独调用beginPath和stroke,且重复设置strokeStyle,这会触发大量Canvas上下文状态更新。将同色路径合并为一个路径后一次性绘制,能大幅减少状态切换开销。缓存渐变对象,避免重复创建
原background()方法每次渲染都重新创建线性渐变,Firefox对这类对象的创建开销敏感。缓存渐变实例,仅在颜色或画布尺寸变化时重新生成。提取固定计算,避免重复运算
将循环内的固定值计算(如radius/3、旋转角度转弧度)移到循环外,减少每帧的重复计算量。优化数组操作
原代码用shift()移除数组首元素,该操作会导致数组内所有元素前移,性能开销大。改用slice(1)替代,或使用环形数组指针进一步优化。调整渲染循环逻辑
修正requestAnimationFrame的调用方式,避免递归调用可能导致的帧堆积;不可见时完全停止渲染流程,而非仅跳过绘制步骤。
修改后核心代码示例
class BG { animationContainer = null; options = { rotation: 45, waves: 3, width: 300, hue: [14, 50], amplitude: .5, background: 1, preload: 1, speed: [.001, .004] }; waves = []; hue = 0; hueFw = true; canvas = null; ctx = null; observer = null; radius = 0; centerX = 0; centerY = 0; width = 0; height = 0; scale = 1; isVisible = false; gradient = null; color = "#000000"; lastColor = ""; lastHeight = 0; constructor(t, e) { this.animationContainer = t; this.options = { rotation: e.rotation || 45, waves: e.waves || 2, width: e.width || 300, hue: e.hue || [17, 50], amplitude: e.amplitude || .75, background: e.background || 1, preload: e.preload || 1, speed: e.speed || [.001, .004] }; this.waves = []; this.hue = this.options.hue[0]; this.hueFw = true; this.setup(); this.render(); } setup() { this.canvas = document.createElement("canvas"); this.ctx = this.canvas.getContext("2d"); this.animationContainer.appendChild(this.canvas); this.resize(); window.addEventListener("resize", this.resize.bind(this)); this.waves = []; for (let t = 0; t < this.options.waves; t++) this.waves.push(WV(this)); this.options.preload && this.preload(); this.addIntersection(); } resize() { const { offsetWidth: t, offsetHeight: e } = this.animationContainer; this.scale = window.devicePixelRatio || 1; this.width = t * this.scale; this.height = e * this.scale; this.canvas.width = this.width; this.canvas.height = this.height; this.canvas.style.width = `${t}px`; this.canvas.style.height = `${e}px`; this.radius = Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2)) / 2; this.centerX = this.width / 2; this.centerY = this.height / 2; } addIntersection() { this.observer = new IntersectionObserver(t => { for (let e of t) this.isVisible = e.intersectionRatio > 0; }); this.observer.observe(this.animationContainer); } preload() { for (let t = 0; t < this.options.waves; t++) { this.updateColor(); for (let e = 0; e < this.options.width; e++) this.waves[t].update(); } } updateColor() { this.hue += this.hueFw ? .01 : -.01; if (this.hue > this.options.hue[1] && this.hueFw) this.hueFw = false; else if (this.hue < this.options.hue[0] && !this.hueFw) this.hueFw = true; const t = Math.floor(127 * Math.sin(.3 * this.hue) + 128); const e = Math.floor(127 * Math.sin(.3 * this.hue + 2) + 128); const i = Math.floor(127 * Math.sin(.3 * this.hue + 4) + 128); this.color = `rgba(${t},${e},${i},0.1)`; } render() { const renderFrame = () => { if (this.isVisible) this.draw(); requestAnimationFrame(renderFrame); }; requestAnimationFrame(renderFrame); } draw() { this.updateColor(); this.clear(); if (this.options.background) this.background(); for (let t of this.waves) { t.update(); t.draw(); } } clear() { this.ctx.clearRect(0, 0, this.width, this.height); } background() { if (!this.gradient || this.lastColor !== this.color || this.lastHeight !== this.height) { this.gradient = this.ctx.createLinearGradient(0, 0, 0, this.height); this.gradient.addColorStop(1, "#0E1218"); this.gradient.addColorStop(0, this.color); this.lastColor = this.color; this.lastHeight = this.height; } this.ctx.fillStyle = this.gradient; this.ctx.fillRect(0, 0, this.width, this.height); } } function LN(t, e) { t.angle[0] += t.speed[0]; t.angle[1] += t.speed[1]; t.angle[2] += t.speed[2]; t.angle[3] += t.speed[3]; return { angle: [ Math.sin(t.angle[0]), Math.sin(t.angle[1]), Math.sin(t.angle[2]), Math.sin(t.angle[3]) ], wave: t, color: e }; } function rng(t, e) { return e === undefined ? Math.random() * t : t + Math.random() * (e - t); } function dr() { return Math.random() > .5 ? 1 : -1; } function WV(t) { let e = []; const angles = [ rng(2 * Math.PI), rng(2 * Math.PI), rng(2 * Math.PI), rng(2 * Math.PI) ]; const speeds = [ rng(t.options.speed[0], t.options.speed[1]) * dr(), rng(t.options.speed[0], t.options.speed[1]) * dr(), rng(t.options.speed[0], t.options.speed[1]) * dr(), rng(t.options.speed[0], t.options.speed[1]) * dr() ]; const h = () => { e.push(LN({ angle: angles, speed: speeds }, t.color)); if (e.length > t.options.width) { e = e.slice(1); } }; const n = () => { const { ctx: i, radius: s, centerX: h, centerY: n, options: o } = t; const r = s / 3; const a = (o.rotation * Math.PI) / 360; const d = o.amplitude; i.strokeStyle = t.color; i.beginPath(); for (const l of e) { const { angle: $ } = l; const wave1 = $[0] * d + a; const wave2 = $[3] * d + a; const wave3 = $[1] * d * 2; const wave4 = $[2] * d * 2; const u = h - s * Math.cos(wave1); const p = n - s * Math.sin(wave1); const w = h - r * Math.cos(wave3); const _ = n - r * Math.sin(wave3); const v = h + r * Math.cos(wave4); const b = n + r * Math.sin(wave4); const c = h + s * Math.cos(wave2); const g = n + s * Math.sin(wave2); i.moveTo(u, p); i.bezierCurveTo(w, _, v, b, c, g); } i.stroke(); }; return { lines: e, angle: angles, speed: speeds, update: h, draw: n }; }
额外建议
如果帧率仍未达标,可以尝试降低options.width的数值(当前为300),减少单帧绘制的贝塞尔曲线数量,这对Firefox的帧率提升效果显著。
内容的提问来源于stack exchange,提问作者axy_david
相关产品推荐
相关产品推荐

