追凶 5000 帧:一次 Hero 页面频闪修复实录
你以为只是改了个角度,浏览器却在背后哭了。
起因
首页 Hero 区域有一个由 SVG 构建的奇门遁甲罗盘——七层同心环以不同速度旋转,中心太极图带动 44 个粒子轨道运动,外层还有鼠标驱动的视差效果。视觉效果很酷,但有一个致命问题:页面一直在闪烁。
我试着调了一下 3D 透视角度,把 rotateX 从 -14° 改到 -22°,perspective 从 1200px 缩到 1000px,scale 从 1.12 拉到 1.23。结果频闪直接恶化到肉眼可见的程度。
问题记录在 HANDOFF.md 里,标注为 UNRESOLVED。是时候正面解决了。
第一轮:表面修复
做了什么
.hero-right加will-change: transform— 把整个 Hero 右侧提升到独立 GPU 合成层- 视差 RAF 惰性化 — 鼠标不动时
cancelAnimationFrame,lerp 收敛后停止循环 - 粒子动画改用
style.transform— 把setAttribute('cx'/'cy')改成 CSStranslate(),避免 SVG 属性变更触发 layout reflow
结果
闪烁减轻了,但没消失。角度调大后问题又回来了。
深入排查:数 RAF 循环
冷静下来,我数了数页面上同时运行的 requestAnimationFrame 循环:
| 循环 | 位置 | 频率 | 每帧 DOM 写入量 |
|---|---|---|---|
| 粒子动画 | TaijiCenter.tsx |
60fps | 176 个 style.transform |
| 鼠标视差 | CosmicHero.tsx |
60fps(鼠标移动时) | 8 个 SVG transform |
| 聚光灯追踪 | SpotlightTracker.tsx |
60fps 持续运行 | 2 个 CSS 自定义属性 |
| 帖子自动滚动 | HomeInteractions.tsx |
60fps | scrollLeft(触发布局) |
| 作品自动滚动 | HomeInteractions.tsx |
60fps | scrollLeft(触发布局) |
| 碎碎念自动滚动 | HomeInteractions.tsx |
60fps | scrollLeft(触发布局) |
总计 6 个 RAF 循环在同时跑。 其中 scrollLeft 写入会触发同步布局计算,代价最高。
再加上 CSS 动画(8 组环旋转 + 7 个呼吸脉冲 + 4 个太极光环脉冲)和 SVG 滤镜(glow、glow-md、glow-lg、taiji-glow),GPU 合成层压力巨大。
当 3D 透视角度变得更陡(rotateX: -22°),浏览器需要处理更复杂的矩阵变换,原本勉强能撑住的帧率直接崩了。
第二轮:根因修复
1. 消除粒子动画 RAF(最大收益)
文件: TaijiCenter.tsx
粒子轨道动画每帧更新 176 个 SVG 元素的 style.transform,即使节流到 30fps 也是每秒 5280 次 DOM 写入。
方案: 删除整个 RAF 循环,粒子位置一次性预计算,固定在随机初始角度上。然后用 CSS 动画给整个粒子组加一个极慢的旋转:
#orbitParticles { transform-origin: 1000px 450px; animation: rotateCW 30s linear infinite; }
#outerParticles { transform-origin: 1000px 450px; animation: rotateCCW 45s linear infinite; }
视觉效果几乎一样——粒子群整体缓慢旋转,但 DOM 写入量从 5280 次/秒降到 0。
2. 聚光灯追踪惰性化
文件: SpotlightTracker.tsx
这个组件在根 layout 里,每个页面都在跑。聚光灯的 radial-gradient 背景每帧都要重新计算 CSS 自定义属性 --mx / --my,即使鼠标完全不动。
方案: 改为和视差一样的惰性模式——只在鼠标移动时启动 RAF,lerp 收敛后 cancelAnimationFrame。
3. 自动滚动合并为单个 RAF
文件: HomeInteractions.tsx
三个独立的 RAF 循环各自跑 scrollLeft 写入,每个都触发同步布局。
方案: 合并为一个 RAF 循环,在一次回调里更新三个滚动容器的位置。从 3 个 RAF 降到 1 个。
4. 视差恢复 setAttribute + 值缓存
文件: CosmicHero.tsx
上一轮把 setAttribute('transform') 改成 style.transform 是个错误——SVG transform 属性使用 viewport 单位,而 CSS transform 使用屏幕像素,在 viewBox 1600×900 映射到不同屏幕宽度时,偏移量计算会出错。
方案: 恢复 setAttribute,但加入字符串值缓存——只有实际变化时才写入 DOM。
5. 移除 transform-style: preserve-3d
文件: globals.css
这个属性强制浏览器为每个 CSS 动画子元素创建独立 GPU 层。8 个旋转环 + 太极组 = 至少 9 个额外层,反而增加了合成压力。
最终战果
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 连续 RAF 循环数 | 6 | 2(1 滚动 + 1 惰性视差) |
| 每秒 DOM 写入量 | ~5500+ | ~60 |
| GPU 层数量 | 15+ | 8 |
| 频闪 | ✅ 持续 | ❌ 消除 |
教训
- SVG 动画不要用 RAF 逐帧更新属性——用 CSS
animation或 SVG<animate>,让浏览器合成器处理 scrollLeft写入是隐形杀手——它触发同步布局,比style.transform贵一个数量级preserve-3d不是银弹——它会创建更多 GPU 层,层数过多反而降低性能- 数 RAF 循环——当你觉得页面卡顿时,第一件事应该是
Ctrl+Shift+J打开控制台,在 Performance 面板看帧率。如果持续低于 55fps,数数有多少个requestAnimationFrame在跑
2026-05-17 · Next.js 16.2.6 · Turbopack · SVG · CSS Compositing