Flutter 性能调优之点击响应和完成时延分析介绍

0 评论 1259 浏览 0 收藏 25 分钟

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:

Flutter 性能调优之点击响应和完成时延分析介绍-华为开发者话题 | 华为开发者联盟

本文档是《Flutter 性能调优》系列的第三篇。若你还不熟悉性能分析工具、trace 采集方式或 UI/Raster 线程的基本识别方法,建议先阅读第一篇《Flutter 性能调优之性能分析工具和 trace 介绍》;若需了解滑动卡顿与丢帧的排查方法,可阅读第二篇《Flutter 性能调优之滑动卡顿问题介绍》。

       本文聚焦 Flutter 页面点击操作后的响应时延与完成时延。你将从 Flutter 渲染管线出发,学习如何在 OHOS 平台 Trace 中圈定点击窗口、逐段拆解响应链路、区分 UI 线程瓶颈与 Raster 线程瓶颈,并掌握三类最常见根因的修复方法:主线程同步重活、首屏一次性构建/加载过多,以及 setState 粒度太大导致的整页重建。每个根因都配有可运行的 Dart 示例、Trace 判定信号和修复前后的效果对比。

       阅读完本文后,你将能够:

          • 独立分析一次点击操作的响应时延与完成时延;

          • 根据 Trace 特征快速怀疑并验证“主线程同步重活”“首屏构建过多”“setState 粒度太大”等根因;

          • 对 UI 线程同步阻塞、一次性构建过重和大范围重建问题给出针对性修复;

          • 在团队中复用本文的排查思路和案例,进行时延问题的标准化定位与优化。

1. 概述

       页面卡顿,除了掉帧引起的动画或滑动卡顿,还有两类常见的时延问题:响应时延过长和完成时延过长。

响应时延一般指的是从手指触摸事件产生,到屏幕上出现第一个可见反馈帧的时间。Flutter 里这通常对应:触摸事件经系统分发到 Flutter 主线程 → 主线程处理并触发 rebuild → 生成 Layer tree → 渲染线程栅格化 → 送屏。

       完成时延一般指的是从用户操作开始,到整个操作彻底完成(页面渲染稳定、数据全部展示)的时间。它包含响应时延,还包括数据加载、图片解码、复杂布局等后续工作。

2. Flutter 渲染管线介绍

       Flutter 渲染一帧经过四个阶段:

阶段

做什么

大致耗在哪条线程

Build

根据状态变化构建/更新 Widget 树、Element 树

UI 线程

Layout

RenderObject 树计算每个节点的大小和位置

UI 线程

Paint

生成绘制指令,构建 Layer tree

UI 线程

Composite

将 Layer tree 栅格化成像素,输出到屏幕

Raster 线程

       UI 线程耗时过长说明 Dart 层有问题;Raster 线程耗时过长说明绘制负载重(阴影、模糊、大图等)。

渲染管线等详细说明,可以见第二篇《Flutter 性能调优之滑动卡顿问题介绍》第 2 章。

3. 基于 OHOS平台Trace 的点击响应/完成时延定位

3.1 响应时延:首帧上屏

trace name

字段

说明

DispatchTouchEvent

type 字段:0=按下、1=抬起、2=移动

触摸事件分发。 点击场景下通常只有一对 down/up,中间可能有少量 move。如果 trace 中有多段点击,每对 down/up 对应一次点击。

       从触摸起点(点击场景取 type=1 离手、触摸/滑动场景取 type=0 按下)开始,向后查找首个完整的渲染帧链路:Animator::BeginFrame → GPURasterizer::Draw → RSMainThread::DoComposition → RSHardwareThread::CommitAndReleaseLayers。首帧到达 CommitAndReleaseLayers 的时刻即为响应时延终点。

3.2 完成时延:首屏稳定

       从首帧上屏后继续向后查看,直到帧渲染活动趋于稳定——即 Animator::BeginFrame 和 GPURasterizer::Draw 不再连续出现,或帧间隔恢复正常刷新周期。最后一帧上屏的时刻即为完成时延终点。

       响应时延由多个环节串联组成,需要逐段度量耗时,找出最长的瓶颈环节:

环节

起止 trace

高耗时含义

下一步深挖方向

多模输入

没有对应的trace_name,但是有对应的线程mmi_service

系统侧触摸事件

不涉及

触摸事件分发

mmi_service → DispatchTouchEvent type=1/0

系统侧分发触摸事件到应用接收

不涉及

事件分发

DispatchTouchEvent type=1 → Engine::DispatchPointerDataPacket

系统层事件分发延迟

检查 touchEventDispatch、系统侧事件积压

Vsync 等待

Engine::DispatchPointerDataPacket → VsyncFireCallback

触摸发生在两个 Vsync 之间,需等下一个

正常现象;若 >1 帧预算则异常

UI 线程处理

VsyncFireCallback → Animator::BeginFrame → 完成 build/layout/paint

Dart 层 build 过重或被同步重活阻塞

检查 begin_frame_ / draw_frame_,确认 BUILD/LAYOUT/PAINT 哪个阶段最重

Raster 栅格化

GPURasterizer::Draw

绘制过重或 shader/glyph 冷编译

检查 CreateGlyphAtlas、Load/Compile Shaders、SurfaceFrame::Submit

应用→渲染服务

SurfaceFrame::Submit → RSMainThread::ProcessCommandUni[pid,seq]

引擎到渲染服务的提交与调度延迟

用 transactionFlag↔pid,seq 对齐、查 buffer 可用性

合成上屏

RSMainThread::DoComposition → CommitAndReleaseLayers

系统层合成负载重

检查合成器层级、fence 等待

       多数情况下瓶颈在 UI 线程处理段(build 过重或同步重活阻塞)或 Raster 栅格化段(shader/glyph 冷编译)。事件分发和 Vsync 等待通常较短,若这两段长则属于系统层问题。

3.3 UI 线程瓶颈

       在收窄窗口内,按 UI 线程 tid 过滤 slices,重点关注以下 slice,确认哪个阶段最重:

slice 名称

说明

指向的根因

BUILD

Widget 树的构建阶段

首屏一次性构建过多、setState 触发大范围重建、itemBuilder 创建重量级 Widget

LAYOUT

RenderObject 树的布局阶段

首屏布局复杂、使用 IntrinsicHeight / IntrinsicWidth、大量 Flex/Stack 嵌套

PAINT

生成绘制指令

绘制指令复杂、大量裁剪/复杂 CustomPaint

Animate

动画回调执行

动画回调中做重计算

PlatformConfiguration

平台配置/消息处理

Platform Channel 同步调用阻塞

BeginFrame 整体长但子 slice 不明显

一帧开始,调度 BUILD/LAYOUT/PAINT 各阶段

主线程被同步重活阻塞(如 JSON 解析、文件 IO、复杂计算)

       如果上述 slices 中缺少 BUILD/LAYOUT/PAINT,说明 trace 未启用 –trace-systrace,UI 线程归因有限。需在 FlutterLoader.ets 中添加 –trace-systrace 引擎参数后重新采集。

根因 1:主线程做同步重活

       一句话结论:点击后如果 UI 线程被同步计算、IO 或解析阻塞,首帧无法及时开始渲染,表现为白屏或卡死。

现象

       • 点击后界面无反馈,持续白屏或卡死

       • 按钮长时间停留在按下状态

       • 内容在阻塞结束后一次性全部出现

Trace 判定信号

trace 特征

说明

BeginFrame 整体长,但 BUILD/LAYOUT/PAINT 子 slice 不明显

UI 线程被其他同步任务占用

PlatformConfiguration / DartIsolate::HandleMessage 异常长

主线程正在处理大量平台消息或 Dart 消息

点击后 Animator::BeginFrame 迟迟不出现,或首帧远超帧预算

首帧被同步重活阻塞

期间 GPURasterizer::Draw 几乎无活动

Raster 线程在等待 UI 线程产出 Layer tree

代码验证与修复

       打开按钮 onPressed 或页面入口对应的处理函数,检查是否存在以下同步重活:

          • jsonDecode / xmlDecode 等大体积数据解析

          • rootBundle.loadString / File.readAsStringSync 等同步文件读取

          • 复杂数学运算、大量字符串处理、图片编码等 CPU 密集型操作

          • 上述操作在 setState 之前同步执行

       若存在,把重活放到后台 Isolate,点击入口立即给反馈:

void _run() async {
  setState(() => _loading = true);
  final result = await compute(_heavyWork, _iterations); // 后台 Isolate
  setState(() {
    _result = result;
    _loading = false;
  });
}

效果验证

维度

问题案例

修复案例

计算位置

主线程同步执行

后台 Isolate(compute)

点击后首帧

卡在按下状态,loading 不出现

立即出现 loading 指示器

界面响应

完全卡死至计算结束

始终可交互

完成时延

长

短

hitrace截图

根因 2:首屏一次性构建/加载太多

       一句话结论:跳转后首屏一次性构建或加载过多 Widget/数据,导致首帧阻塞,表现为白屏或转圈后内容一次性出现。

现象

       • 页面打开后先白屏/转圈

       • 内容一次性全部出现

       • 完成时延高

Trace 判定信号

trace 特征

说明

首帧 BUILD slice 占比极高

首帧需要构建大量 Widget

BUILD 调用栈中出现大量子节点构建

非懒加载导致一次性构建过多 Element

首帧后仍连续多帧 BeginFrame + Draw

内容分批构建或图片逐帧解码

首屏集中出现 DecodeImage/ImageDecoder

首帧加载并解码了过多图片

代码验证与修复

       检查页面是否使用 ListView(children: …) / Column / Wrap 一次性构建大量子项,或 initState / build 中同步加载大量数据。修复时优先骨架屏占位,并用 ListView.builder 懒加载:

@override
Widget build(BuildContext context) {
  return _loading
  ? ListView.builder(itemCount: 10, itemBuilder: (_, __) => _SkeletonItem())
  : ListView.builder(itemCount: _items.length, itemBuilder: (_, i) => _buildItem(_items[i]));
}

效果验证

维度

问题案例

修复案例

首帧构建

500 个复杂项全量 build

10 个骨架项

ListView 类型

ListView(children:) 非懒加载

ListView.builder 懒加载

首屏反馈

白屏等待全部 build 完成

骨架屏立即上屏

完成时延

长

短

hitrace截图

根因 3:setState 粒度太大,整页重建

       一句话结论:setState 调用位置过高或静态子树未标记 const,导致局部变化触发整页 rebuild,响应/完成时延变长。

现象

       • 点击后页面迟迟没有反馈

       • 刷新一下要等很久内容才稳定

       • 单次 build 函数不复杂,但重建范围很大

Trace 判定信号

trace 特征

说明

点击后 BUILD slice 变长,但单次 build 不复杂

重建范围过大

BUILD 子调用包含大量本不该变化的静态子树

setState 位置过高

每次点击都触发几乎相同范围的 BUILD

状态影响范围未收敛

LAYOUT/PAINT 被连带拉长

rebuild 引发整页重排/重绘

代码验证与修复

       检查 setState 是否放在页面根节点或大范围祖先 Widget,以及静态子树是否未标记 const。把状态下放到最小范围:

// ❌ setState 在页面根,静态子树被迫 rebuild
class BadPage extends StatefulWidget {
  ...
  Widget build(BuildContext context) {
    return Column(
      children: [
        HeavyHeader(), // 没变,却被迫 rebuild
        Expanded(child: HeavyList()), // 没变,却被迫 rebuild
        Text('$_count'),
        ElevatedButton(
          onPressed: () => setState(() => _count++),
          child: const Text('+1'),
        ),
      ],
    );
  }
}

// ✅ 静态子树 const,Counter 自己 rebuild
class GoodPage extends StatelessWidget {
  const GoodPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        HeavyHeader(),
        Expanded(child: HeavyList()),
        Counter(),
      ],
    );
  }
}

效果验证

维度

问题案例

修复案例

重量级内容

Wrap(300项)

const Wrap(300项)

+1 后重建

300 项全部重建

仅计数器

+1 重建开销

~15-20ms

<1ms

响应时延

长

短

hitrace截图

3.4 Raster 线程瓶颈

       在收窄窗口内,按 Raster 线程 tid 过滤 Flutter/Impeller 相关 slices,重点观察以下 slice 是否出现及其耗时:

slice 名称

说明

指向的根因

impeller::CreateGlyphAtlas / UpdateAtlasBitmap

字形图集的创建与更新

字形图集冷启动(首次渲染新字形)

Load/Compile Shaders / bisheng_graphic_pipeline_compilation

着色器加载与编译

着色器冷编译(首次使用新 shader)

SurfaceFrame::Submit / Encode

帧的提交与编码

场景复杂、绘制指令过多

AcquireNextImageKHR / vkQueueSubmit

GPU 图像获取与命令队列提交

GPU 资源竞争或驱动瓶颈

DecodeImage / ImageDecoder 相关 slice

图片解码

图片按原尺寸解码、未使用 cacheWidth/cacheHeight

       其他常见 Raster 瓶颈(大图解码、saveLayer 过度绘制、阴影模糊等)。

根因 4:大图按原尺寸解码

       一句话结论:图片按原始分辨率解码,Raster 线程解码/上传耗时远高于实际显示所需,导致首屏图片延迟或完成时延高。

现象

       • 点进页面后图片迟迟不显示

       • 下拉刷新后内容稳定很慢

       • 图片首次显示丢帧,后续正常

Trace 判定信号

trace 特征

说明

GPURasterizer::Draw 或图片解码相关 slice 明显变长

图片解码占用大量 Raster 线程时间

出现 DecodeImage / ImageDecoder / UploadTexture

引擎正在解码或上传大尺寸图片

UI 线程正常,但图片区域长时间空白

瓶颈在图片解码而非 Dart 层构建

图片首次显示丢帧,后续正常

首次解码成本高,缓存后不再复现

代码验证与修复

       检查图片加载是否未指定 cacheWidth / cacheHeight,或原始分辨率远大于显示尺寸。按目标显示尺寸解码:

// ❌ 原尺寸解码
Image.memory(imageBytes, height: 200, fit: BoxFit.cover)

// ✅ 按显示尺寸解码(物理像素)
Image.memory(
  imageBytes,
  height: 200,
  fit: BoxFit.cover,
  cacheWidth: 800,
  cacheHeight: 600,
)

效果验证

维度

问题案例

修复案例

解码分辨率

4000×3000(原尺寸)

800×600

解码像素量

1200 万

48 万(约 1/25)

显存占用

高

低

完成时延

长

短

hitrace截图

hitrace 细节

       下面从 hitrace 细节逐一分析。先把 UI 线程和 Raster 线程收藏排序,本案例涉及图片解码,还会有 IO 线程或 IO Worker 线程参与。

 

问题案例

修复案例

线程概览图

       关于图片的原图信息,可以在 UI 线程中找到。Image MakeFromDataOHOS 展示原图内存大小,Image GetInfo:ImageGenerate-0xxxxxxxxx:(size-4000*3000,…) 展示解码申请的内存大小。

 

问题案例

修复案例

原图内存大小

 

原图尺寸大小

       接下来看图片解码申请的耗时和内存大小。Image CreateExternalTextureSourceOHOS:ImageGenerate-0xxxxxxxx:(size-4000*3000,…) 展示解码申请的耗时和原图尺寸大小;vkAllocateMemory: xxxxx 展示系统侧解码图片申请的内存大小。

 

问题案例

修复案例

图片解码耗时

解码申请的内存大小

4. 参考与延伸

       • Flutter 官方性能文档

       • DevTools Performance 指南

       • 鸿蒙性能调优工具简介

       • 深入性能分析(配套三篇):

          – 性能分析第一步-梳理线程顺序

          – 性能分析-帧渲染跟踪

          – 性能分析-滑动响应时延

          – Flutter 页面滑动卡顿与掉帧问题介绍

          – Flutter 点击响应和完成时延分析介绍

🔗 官网开发者学堂视频:https://developer.huawei.com/consumer/cn/training/result?type2List=201783644516849879&orderBy=1&courseType=5

     🔗 社区DFX专题文章:  https://developer.huawei.com/consumer/cn/forum/subject/2101218731402391001

【扫码加入 HarmonyOS DFX 技术交流群】

本文由 @华为开发者联盟 授权发布于人人都是产品经理。未经作者许可,禁止转载

题图来自Unsplash,基于CC0协议

该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
海报
评论
评论请登录
  1. 目前还没评论,等你发挥!