You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 22:25:48