返回博客

SwiftUI 动态渐变边框叠在 Material 上,风扇狂转 GPU 拉满

前段时间在做一个 macOS 应用的玻璃态 UI,设计稿里有一个带"光环扫边"动效的弹窗卡片。Web 原型用的是 magicui 的 BorderBeam——conic-gradient 配合 CSS @keyframes 让高光无限旋转,浏览器里跑得相当顺滑。于是我照着思路在 SwiftUI 里复刻了一个旋转 AngularGradient,叠在 .ultraThinMaterial 背景上。

结果一打开这个弹窗,风扇立刻起飞,Activity Monitor 里 GPU 直奔 90%,界面还不断闪烁。关掉弹窗,一切恢复平静。

现象

  • 带动效的弹窗一挂载,整个 UI 开始肉眼可见地闪烁(约 1 秒闪一下)
  • Activity Monitor 显示 GPU 占用从 < 5% 飙到 80-90%,CPU 也跟着上涨
  • pkill 重启应用后,只要不打开那个弹窗,一切正常;弹窗一出现症状立刻重现
  • 动画本身还有个额外 bug:通过 .animation(value:) 驱动 AngularGradient(angle:) 根本不会平滑插值,旋转看起来是跳帧的

根因

问题出在「动画层 + 模糊层同时叠加」这个组合上。

SwiftUI 的 .ultraThinMaterial(底层是 NSVisualEffectView)需要实时对视图下方的内容做高斯模糊,这个合成操作必须在 offscreen pass 里完成。每次动画触发重绘,模糊层就必须跟着重算一次——以 60fps 持续旋转意味着每秒 60 次完整的离屏模糊合成,永无止境。

CSS 的 conic-gradient + @keyframes 之所以在浏览器里"免费",是因为浏览器的合成线程(compositor thread)可以直接 promote 到 GPU layer,把旋转动画和 backdrop-filter 的合成解耦。Native CoreAnimation 的合成模型不同,这种解耦对 NSVisualEffectView 不适用。

坑:把 CSS 动画 1:1 移植到 SwiftUI,性能模型完全不同

浏览器里 conic-gradient 旋转 + backdrop-filter 几乎没有 GPU 开销,是因为 compositor 做了专门优化。Native SwiftUI/AppKit 没有对等机制——动效层每旋转一帧,底部 Material 模糊层就必须整体重新合成一次。帧率 × 离屏合成成本 = 风扇和 GPU 满载。

正解

最直接的解法:放弃连续旋转动画,改用静态渐变描边。视觉上仍然能呈现"发光边框"的质感,GPU 开销归零。

// 替换掉 .repeatForever 旋转的 AngularGradient
.overlay(
    RoundedRectangle(cornerRadius: 18)
        .strokeBorder(
            LinearGradient(
                colors: [
                    .accentColor.opacity(0.55),
                    .white.opacity(0.35),
                    .white.opacity(0.35),
                ],
                startPoint: .top,
                endPoint: .bottom
            ),
            lineWidth: 1.2
        )
)

静态光效在玻璃态 UI 里其实已经足够好看——顶部亮、底部暗,和光源逻辑吻合,不需要动起来。

如果业务上确实需要运动感,可以把动效层移到 Material 层之外,用 .rotationEffect 旋转整个渐变描边视图,同时确保动效层不与模糊背景产生 offscreen 合成依赖。但代价仍然存在,弹窗类场景优先选静态方案。

另外记住:AngularGradient(angle:)angle 参数本身不支持隐式动画平滑插值,要旋转渐变效果只能对整个 layer 做 .rotationEffect,不能直接 animate 角度属性。

正解:静态渐变描边替换旋转动效,GPU 降回空闲

.repeatForever 动画的 AngularGradient 替换为一次性绘制的 LinearGradient 描边。视觉上保留发光边框质感,GPU 占用从 80%+ 归零,界面闪烁消失。

一句话外卖

Native 玻璃态(Material/blur)+ 持续动画 = 性能毒药;浏览器合成层优化对 SwiftUI/AppKit 毫无参考价值,移植 CSS 动效前先验 GPU 开销。