悟夕导航

第六集:性能翻车现场——十万颗粒子如何不卡成 PPT

101 0 0
“老板说要‘百万星空’,我跑到 5 万帧率掉到 3,他以为我在放 PPT 逐帧动画。”
—— 某位把 CPU 跑成吹风机的前端小可爱

大家好,欢迎来到《Canvas 翻车指南》第六集!
前面我们鼠标一挥,烟花放得很爽,但粒子一多就卡成狗。今天上硬货:

  • WebWorker:把物理计算踢出主线程
  • 顶点着色器 trick:GPU 批量渲染
  • OffscreenCanvas + transferControlToOffscreen:144Hz 电竞屏都不掉帧

准备好护肝片,咱们把十万星辰塞进浏览器,还让风扇不转


一、性能瓶颈诊断:为什么我的 5 万粒子只有 5 帧?

先画个“性能黑板”:

耗时元凶占比(DevTools)原因
for 循环更新粒子60 %JS 计算 O(n)
ctx.arc() 绘制30 %每调用一次走一次 CPU 路径
clearRect + fillRect 全屏清屏10 %填充 2K 屏像刷墙

结论:

“CPU 既当爹又当妈,活该秃顶。”

二、WebWorker:把物理计算踢到隔壁线程

1. 新建 particleWorker.js

self.onmessage = function (e) {
  const { particles, dt } = e.data;
  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vy += 0.1; // 重力
    p.life -= 0.01;
  }
  self.postMessage(particles, [particles.buffer]); // 转移所有权
};

2. 主线程只负责画画

const worker = new Worker('particleWorker.js');
worker.postMessage({ particles: buffer, dt: 16 });

worker.onmessage = (e) => {
  particleArray = e.data; // 直接拿来渲染
};

CPU 耗时从 16 ms 跌到 2 ms主线程风扇瞬间安静像给 Mac 装上了水冷

口诀:

“Worker 不碰 DOM,只搞数据,像外包不抢你工位。”

三、GPU 批量渲染:把 10 万次 arc() 变成 1 次 drawArrays()

1. 启用 WebGL 上下文(2D 上下文直接跪)

<canvas id="webgl" width="800" height="600"></canvas>
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

2. 顶点着色器:把粒子当点精灵(POINTS)

attribute vec2 a_position;
attribute float a_size;
attribute vec3 a_color;
varying vec3 v_color;
uniform vec2 u_resolution;

void main() {
  vec2 zeroToOne = a_position / u_resolution;
  vec2 zeroToTwo = zeroToOne * 2.0;
  vec2 clipSpace = zeroToTwo - 1.0;
  gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
  gl_PointSize = a_size;
  v_color = a_color;
}

3. 片元着色器:画圆

precision mediump float;
varying vec3 v_color;
void main() {
  vec2 coord = gl_PointCoord - vec2(0.5);
  if (length(coord) > 0.5) discard; // 裁成圆
  gl_FragColor = vec4(v_color, 1);
}

4. 一次上传全部顶点

const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW);
gl.drawArrays(gl.POINTS, 0, particleCount);

10 万粒子 → 1 次 draw call帧率 60 稳如老狗GPU 笑着说你再来点


四、OffscreenCanvas:让 RAF 跑到后台线程

主线程新建:

const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('renderWorker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);

renderWorker.js 里照样 RAF:

self.onmessage = function (e) {
  const canvas = e.data.canvas;
  const gl = canvas.getContext('webgl');
  function loop() {
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.POINTS, 0, count);
    requestAnimationFrame(loop);
  }
  loop();
};

主线程完全解放还能去跑 React 虚拟 DOM双线程并行,互不打扰

口诀:

“OffscreenCanvas 就像给浏览器开了小号,性能爆表不封号。”

五、合体技:CPU + GPU 双线程流水线

  1. WebWorker 算物理 → 每 16 ms 更新坐标
  2. Transfer 最新坐标到 GPU Buffer
  3. Offscreen WebGL 线程 RAF 渲染
  4. 主线程只收 FPS 计数器顺便刷微博

实测:

  • 15 万粒子 60 FPS
  • MacBook Air M1 风扇 0 转
  • 内存 300 MB(32 位颜色)

六、翻车警告:显存爆炸 & 苹果胶水芯片

  • 100 万粒子 = 100 万顶点 × 28 byte ≈ 28 MB 显存老核显直接黑屏
  • iOS 15 以下不支持 transferControlToOffscreen需要优雅降级到 2D + 3 万粒子
  • 安卓微信内置内核WebGL 2.0 不全先测再上线,不然被用户拉黑

七、本集知识点一口闷

优化口诀翻车点
WebWorker算料外包postMessage 大数据要 Transfer
WebGL POINTS一 call 画十万片元裁圆忘记 discard
OffscreenCanvas小号渲染iOS 低版本不识别
显存百万顶点 28 MB老核显直接跪

八、课后作业(翻车 field)

  1. 用 WebGL 画十万星空加地心引力鼠标移动生成黑洞吸星
  2. FPS 面板:每帧计算 performance.now()低于 55 标红
  3. BonusWorker + WebGL 双线程粒子撞墙弹跳录屏晒 144Hz 稳定截图

九、下集预告

第七集《把 Canvas 塞进 React/Vue,前端框架不打架》
带你上:

  • Hook + Ref 管理画布生命周期
  • 自定义指令封装“粒子组件”
  • Vite 热更新不丢 WebGL 上下文,保存状态不闪屏

“人生就像 OffscreenCanvas,
你不主动开小号,
就只能在大号里被生活锁 60 FPS。”

—— 我说的。

第六集结束,点赞、转发、晒粒子数
下集一起把 Canvas 塞进框架,回见!

0
快来点个赞吧

发表评论

隐私评论

评论列表

来写一个评论吧