在日常开发中,我们难免会因为一些不可控的原因导致我们无法使用最新版本的flutter ,这导致在进行开发时,我们并不能选择使用Impeller来进行渲染只能继续使用skia,那面对动画卡顿,我们就束手无策了么?当然不是,面对这个问题,我们有多种方案来进行优化,下面我着重介绍一下用来消除动画卡顿(shader compilation jank)的成熟方案ShaderWarmUp.warmUpOnCanvas 和 SkSL 。
当我们使用Skia 渲染引擎,首次绘制某个 shader(如渐变、Blend、FragmentShader)时,Skia 会编译 GPU 程序,这可能耗时高达 几十到几百毫秒,导致动画掉帧(jank)。这种现象在应用首次启动或者动画首次播放时尤为明显。
为了解决这个问题,Flutter提供了抽象类 ShaderWarmUp,在PaintingBinding.initInstances()阶段自动调用其execute()方法,开发者可继承该类并实现 Future warmUpOnCanvas(Canvas canvas) 方法,该方法绘制触发 shader 编译缓存(offscreen canvas),并等待编译完成后才继续渲染首帧,我们通过预编译 GPU shader,避免运行时 jank。
接下来我会通过一段简单的代码实现一个渐变文字的动画来比较是否使用该方法来进行预热的区别,下面是具体的效果:

首先我们不进行任何优化,当应用启动时,我们在devtool中观察到了明显的暗红色。


接下来我们编写一段shader warm-up的代码:
scala
class MyShaderWarmUp extends ShaderWarmUp {
const MyShaderWarmUp();
@override
Future<void> warmUpOnCanvas(Canvas canvas) async {
print('🔥 warmUpOnCanvas triggered');
final paint = Paint()
..shader = ui.Gradient.linear(Offset.zero, Offset(200, 200), [
Colors.red,
Colors.blue,
]);
canvas.drawRect(const Rect.fromLTWH(0, 0, 200, 200), paint);
}
}
并且在初始化之前进行绑定赋值:
ini
PaintingBinding.shaderWarmUp = const MyShaderWarmUp();
runApp(...);
结果如下图所示:


当我们使用这种方式进行shader warm-up后,shader 编译在启动时完成,后续无抖动。 warmUpOnCanvas 只执行一次,专用于启动阶段预热,不会在动画阶段重复调用 这种方法的优势是控制精确,可覆盖广泛的动作。但是编写成本高,在编写预热代码时需要模拟真实场景中的操作,如果app中使用了多个shader类型(比如多个 fragment shader、特效 shader),可能需要编写大量的预热代码以覆盖相关 shader compile。 接下来我来介绍一下另一种工具化、跨设备适配能力更强的解决方案------SkSL文件预热方案。
整个流程并不复杂,首先在真实的设备上使用命令运行你的app(因为模拟器上捕获的着色器可能无法在实际硬件上正确工作):
css
flutter run --profile --cache-sksl --purge-persistent-cache
这里解释一下相关的参数:
- -profile:用于性能分析,避免 Debug 模式的影响
- -cache-sksl:启用 SkSL 收集
- -purge-persistent-cache:清空旧缓存以确保准确性
之后使用app执行相关的动画,触发UI变化。(这是因为SkSL数据是在运行时捕获的,只有当着色器被实际编译和使用时,它们才会被记录下来),当你觉得已经触发了足够多的动画并且记录了大部分着色器的操作之后,回到运行flutter run的命令行窗口,按下键盘上的M键,测试,命令行会显示一条信息,表示SkSL数据已被写入一个文件中。
之后将捕获到的SkSL文件路径传递给构建命令:
Android:
ini
flutter build apk --bundle-sksl-path=xxx.sksl.json
iOS:
ini
flutter build ios --bundle-sksl-path=xxx.sksl.json
构建成功后,Shader 会在首次启动时预编译加载,无需运行时编译。
当然这种方法也有它的局限性,SkSL 缓存可能因 GPU 架构不同而失效,Android设备多种多样,缓存可能只适用于部分设备,而iOS的新设备通常是使用Metal,但是SkSL warm-up仅支持OpenGL。
并且不论采用上述哪种方法都有可能增加APP启动的时间。
对比总结
方案 | 执行时机 | Shader 编译时机 | 优势 | 局限性 |
---|---|---|---|---|
ShaderWarmUp | 启动阶段调用一次 | 启动前 offscreen 编译 | 精确控制、适配复杂自定义 Scene | 编写复杂、需要覆盖所有 shader 场景 |
SkSL Warm-up | 捕获脚本一次性完成 | 首次启动加载 SkSL 编译 | 易自动化、可纳入 CI/CD | GPU 兼容性不确定、iOS Metal 不支持、启动慢 |
总的来说,对于使用 Skia 渲染器的应用,如果观察到首次动画掉帧问题,ShaderWarmUp 和 SkSL warm-up 都是有效手段。如果希望内容覆盖更多平台、降低手动实现门槛,SkSL warm-up 更适合 CI/CD 集成。若你已有自定义复杂 shader 或 FragmentShader,结合 ShaderWarmUp.warmUpOnCanvas() 能更精细控制。