这个博客的首屏是一片 GPU 驱动的区块晶体网络。它最初的版本在 Retina 屏上明显卡顿——而问题几乎不在 GPU。
真正的瓶颈:每帧 CPU 重写
旧实现每一帧都在主线程上做两件昂贵的事:
- 对每个实例
setMatrixAt()重写变换矩阵; - 重建所有哈希连线的顶点缓冲。
// 反例:每帧 CPU 重算 + 上传,主线程被打爆
for (let i = 0; i < count; i++) {
dummy.position.copy(flow(base[i], t));
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true; // 每帧全量上传
把位移交给顶点着色器
更好的做法是:把基准坐标作为 instanced attribute 一次性传上去,位移、传播脉冲全部在顶点着色器里用 uTime 计算。JS 每帧只更新一个 uniform。
attribute vec3 aBase;
uniform float uTime;
vec3 flow(vec3 b){
return vec3(
sin(uTime*0.42 + b.y*0.5) * 0.17,
sin(uTime*0.6 + b.x*0.45) * 0.34,
cos(uTime*0.5 + b.x*0.3) * 0.17
);
}
void main(){
vec3 p = position + aBase + flow(aBase);
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}
配合几个常被忽视的细节:
setPixelRatio(min(dpr, 1.5))——Retina 是首屏卡顿的头号原因;IntersectionObserver在首屏离开视口时暂停渲染循环;- 半分辨率 bloom,且只在桌面强机开启;
- 首帧前
renderer.compile()预编译,避免首次滚动 jank。
经验法则:动画一旦逐帧触碰 CPU 上的「每个对象」,就该想办法把它整体搬进 shader。
重写后主线程几乎不再参与逐帧动画,60fps 稳定。这篇就是这次重写的笔记。