第六集:性能翻车现场——十万颗粒子如何不卡成 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 双线程流水线
- WebWorker 算物理 → 每 16 ms 更新坐标
- Transfer 最新坐标到 GPU Buffer
- Offscreen WebGL 线程 RAF 渲染
- 主线程只收 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)
- 用 WebGL 画十万星空,加地心引力,鼠标移动生成黑洞吸星
- FPS 面板:每帧计算
performance.now(),低于 55 标红 - Bonus:Worker + WebGL 双线程,粒子撞墙弹跳,录屏晒 144Hz 稳定截图
九、下集预告
第七集《把 Canvas 塞进 React/Vue,前端框架不打架》
带你上:
- Hook + Ref 管理画布生命周期
- 自定义指令封装“粒子组件”
- Vite 热更新不丢 WebGL 上下文,保存状态不闪屏!
“人生就像 OffscreenCanvas,
你不主动开小号,
就只能在大号里被生活锁 60 FPS。”
—— 我说的。
第六集结束,点赞、转发、晒粒子数,
下集一起把 Canvas 塞进框架,回见!
0
快来点个赞吧
发表评论
评论列表