Flutter Web启动页动画卡顿求助(Safari浏览器尤甚)
Flutter Web启动页动画卡顿(Safari尤为明显)优化方案
核心问题定位
卡顿根源集中在三个方面:
- CustomPaint实时绘制超大圆形,Web端(尤其是Safari)Canvas渲染开销极高
- 带大模糊值的动态BoxShadow,尺寸变化时阴影计算成本巨大
- 高画质图片的渲染开销叠加动画帧消耗,进一步拖慢帧率
针对性优化方案
1. 替换CustomPaint为AnimatedContainer实现圆形展开
放弃实时Canvas绘制,改用AnimatedContainer做缩放动画,利用Flutter Web的CSS硬件加速特性:
// 替换原CustomPaint部分 Positioned.fill( child: Center( child: AnimatedContainer( duration: Duration.zero, // 由AnimationController驱动动画曲线 curve: Curves.easeInOutCirc, width: circleScale * _radius.value, height: circleScale * _radius.value, decoration: BoxDecoration( shape: BoxShape.circle, color: siteMode.splashBcgColor, ), ), ), )
2. 移除动态BoxShadow,改用静态方案
带blurRadius:80的动态阴影是Web端性能杀手,建议:
- 若阴影无需随尺寸缩放,将阴影移至父级静态Widget
- 必须动态的话,提前将阴影+Logo合成一张图片,直接缩放图片避免实时计算阴影
修改后的Logo容器:
AnimatedContainer( duration: Duration.zero, curve: Curves.easeInOutCirc, width: _width.value, height: _width.value, child: Image.asset( siteMode.splashGoatLogo, fit: BoxFit.contain, opacity: _opacity, cacheWidth: 250, cacheHeight: 250, filterQuality: FilterQuality.medium, // 降低过滤质量减少开销 ), )
3. 缓存MediaQuery计算结果
避免每帧重复计算屏幕尺寸,将计算逻辑移至didChangeDependencies:
// 在SplashScreenLogo的State类中缓存 late double circleScale; @override void didChangeDependencies() { super.didChangeDependencies(); final size = MediaQuery.of(context).size; circleScale = size.width > size.height ? size.width *1.2 : size.height *1.2; }
4. 降低图片渲染开销
- 将
filterQuality改为FilterQuality.medium或low,Web端高画质过滤对性能影响显著 - 优先使用WebP格式图片,更小体积更快解码速度
5. 优化动画重建范围
确保AnimatedBuilder只重建必要元素,避免整棵Widget树重复渲染(代码见下方完整片段)
优化后完整关键代码片段
class StaggerAnimation extends StatelessWidget { StaggerAnimation({super.key, required this.controller, required this.circleScale}) : _radius = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: controller, curve: const Interval(0.0, 0.4, curve: Curves.easeInOutCirc), ), ), _opacity = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: controller, curve: const Interval(0.3, 0.6, curve: Curves.easeInOutCirc), ), ), _textOpacity = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: controller, curve: const Interval(0.4, 0.8, curve: Curves.easeInOutCirc), ), ), _width = Tween<double>(begin: 0.0, end: 250.0).animate( CurvedAnimation( parent: controller, curve: const Interval(0.0, 0.6, curve: Curves.easeInOutCirc), ), ); final Animation<double> controller; final double circleScale; final Animation<double> _opacity; final Animation<double> _width; final Animation<double> _radius; final Animation<double> _textOpacity; Widget _buildAnimation(BuildContext context, Widget? child) { return Stack( clipBehavior: Clip.none, // 移除硬裁剪减少开销 alignment: AlignmentDirectional.center, children: [ // 用AnimatedContainer替换CustomPaint Positioned.fill( child: Center( child: AnimatedContainer( duration: Duration.zero, curve: Curves.easeInOutCirc, width: circleScale * _radius.value, height: circleScale * _radius.value, decoration: BoxDecoration( shape: BoxShape.circle, color: siteMode.splashBcgColor, ), ), ), ), // Logo容器(移除动态阴影) AnimatedContainer( duration: Duration.zero, curve: Curves.easeInOutCirc, width: _width.value, height: _width.value, child: Image.asset( siteMode.splashGoatLogo, fit: BoxFit.contain, opacity: _opacity, cacheWidth: 250, cacheHeight: 250, filterQuality: FilterQuality.medium, ), ), // 文本Logo Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 50), child: Image.asset( siteMode.splashTextLogo, opacity: _textOpacity, width: 170, cacheWidth: 170, fit: BoxFit.contain, filterQuality: FilterQuality.medium, ), ), ), ], ); } @override Widget build(BuildContext context) { return AnimatedBuilder( builder: _buildAnimation, animation: controller, ); } } class _SplashScreenLogoState extends State<SplashScreenLogo> with TickerProviderStateMixin { late AnimationController _controller; late double circleScale; @override void initState() { super.initState(); _controller = AnimationController(duration: const Duration(milliseconds: 4000), vsync: this); } @override void didChangeDependencies() { super.didChangeDependencies(); final size = MediaQuery.of(context).size; circleScale = size.width > size.height ? size.width *1.2 : size.height *1.2; } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { _controller.forward(); return StaggerAnimation(controller: _controller.view, circleScale: circleScale); } }
额外建议
- 在Safari中启用CanvasKit渲染器,性能远优于默认HTML渲染器:在index.html中添加
<script>window.flutterWebRenderer = "canvaskit";</script> - 用Flutter DevTools性能面板查看每帧绘制开销,定位剩余瓶颈
内容的提问来源于stack exchange,提问作者Feinliebe
相关产品推荐
相关产品推荐

