Lee's Blog
  • 首页
  • 博客
  • 作品
  • 成长
  • 学习
  • 打榜
  • 练习
  • 关于
登录 / 注册
✦

保持联系

关注我的最新动态

GitHubbilibili

© 2026 Lee's Blog

首页/博客/博客开发/追凶 5000 帧:一次 Hero 页面频闪修复实录
追凶 5000 帧:一次 Hero 页面频闪修复实录
博客开发2026年5月18日5 分钟阅读

追凶 5000 帧:一次 Hero 页面频闪修复实录

6 个 RAF 循环同时跑、每秒 5000+ 次 DOM 写入、3D 透视角度一调就崩——一次完整的 Hero 页面频闪排查实录。

性能优化前端教程
X微博

追凶 5000 帧:一次 Hero 页面频闪修复实录

你以为只是改了个角度,浏览器却在背后哭了。


起因

首页 Hero 区域有一个由 SVG 构建的奇门遁甲罗盘——七层同心环以不同速度旋转,中心太极图带动 44 个粒子轨道运动,外层还有鼠标驱动的视差效果。视觉效果很酷,但有一个致命问题:页面一直在闪烁。

我试着调了一下 3D 透视角度,把 rotateX 从 -14° 改到 -22°,perspective 从 1200px 缩到 1000px,scale 从 1.12 拉到 1.23。结果频闪直接恶化到肉眼可见的程度。

问题记录在 HANDOFF.md 里,标注为 UNRESOLVED。是时候正面解决了。


第一轮:表面修复

做了什么

  1. .hero-right 加 will-change: transform — 把整个 Hero 右侧提升到独立 GPU 合成层
  2. 视差 RAF 惰性化 — 鼠标不动时 cancelAnimationFrame,lerp 收敛后停止循环
  3. 粒子动画改用 style.transform — 把 setAttribute('cx'/'cy') 改成 CSS translate(),避免 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
频闪 ✅ 持续 ❌ 消除

教训

  1. SVG 动画不要用 RAF 逐帧更新属性——用 CSS animation 或 SVG <animate>,让浏览器合成器处理
  2. scrollLeft 写入是隐形杀手——它触发同步布局,比 style.transform 贵一个数量级
  3. preserve-3d 不是银弹——它会创建更多 GPU 层,层数过多反而降低性能
  4. 数 RAF 循环——当你觉得页面卡顿时,第一件事应该是 Ctrl+Shift+J 打开控制台,在 Performance 面板看帧率。如果持续低于 55fps,数数有多少个 requestAnimationFrame 在跑

2026-05-17 · Next.js 16.2.6 · Turbopack · SVG · CSS Compositing

← 上一篇AI 分不清左右?记一次 CSS 3D 旋转的调优经历下一篇 →奶奶说我长得越来越像女孩子了

相关文章

  • 一次登录 / 资料 / 头像的全面体检——5 个用户报告 + curl 抓出的真问题约 9 分钟
  • 用户登录后看不到自己的头像——一次 DOM 顺序撞车约 5 分钟
  • TOC 悬浮折腾记:6 次 commit 才彻底搞定约 6 分钟

评论

需要登录账号才能发表评论。

  • 加载中…

本页内容

  • 追凶 5000 帧:一次 Hero 页面频闪修复实录
  • 起因
  • 第一轮:表面修复
  • 做了什么
  • 结果
  • 深入排查:数 RAF 循环
  • 第二轮:根因修复
  • 1. 消除粒子动画 RAF(最大收益)
  • 2. 聚光灯追踪惰性化
  • 3. 自动滚动合并为单个 RAF
  • 4. 视差恢复 `setAttribute` + 值缓存
  • 5. 移除 `transform-style: preserve-3d`
  • 最终战果
  • 教训