HarmonyOS 布局性能优化:减少节点数的正确姿势
一、今天唠唠布局性能
用 ArkUI 开发过应用的都知道,页面一卡,用户体验直接拉胯。
但你知道为啥会卡吗?说白了,就是参与布局的节点太多了。
今天就把 ArkUI 框架的执行流程给你捋明白,告诉你咋精简节点数,让页面飞起来。
二、ArkUI 是咋工作的
组件树结构
你用 ArkUI 写的界面,本质上是一棵树:
- 布局组件是中间节点
- 基础组件是叶子节点
这棵树叫"应用组件树"。用户滑动、点击的时候,会触发这棵树重新渲染,界面就更新了。

两个更新过程
界面更新分两步:
1. 数据处理过程
更新@State 这些状态数据。数据变化有耗时,关联的组件越多,下一步 UI 更新也越慢。所以别搞无效的数据更新。
2. UI 更新过程
对需要更新的元素执行操作,经历四个阶段:
- Build:组件创建和标脏(标记需要更新的组件)
- Measure:测量组件宽高
- Layout:在屏幕上摆放元素位置
- Render:提交绘制
初次进入页面时,所有组件都会参与渲染(可以理解为都要更新)。
三、组件标脏是啥玩意儿
当组件属性状态变化时,框架会把它标记为"脏"状态,表示需要重新构建。
标脏分两种:
- 布局脏:width/height/padding/margin 这些布局属性变了,标记为布局脏,找到布局边界,进行子树更新
- 样式脏:color/backgroundColor/opacity 这些非布局属性变了,只影响自身,不查找子树
布局边界
如果某个组件布局变了,最简单的方法是对整棵树重新布局。但代价太大啊!
标脏就是用来确定布局最小影响范围的,这个范围就是"布局边界"以内。
一般来讲,如果一个组件设置了固定宽高,那这个组件就是布局边界。内部组件布局变化不会影响外部,查找时只需要在边界内部判断哪些组件会受影响。
确定实际的脏节点数组后,根据脏节点数组来拿到对应的脏节点对象,通过递归遍历 children 进行 Measure 过程,如果该对象布局参数没有发生变化,就会跳过对应的 Measure 阶段。当 Measure 执行完成后,进行 layout 阶段。

从以上的过程可以看出,影响 UI 更新过程的主要因素是参与更新的节点数量。
四、精简节点数的正确姿势
测试数据说话
有人模拟了 10、100、500、1000 层 Row 嵌套的情况,测试首帧绘制和 Measure/Layout 时间:
// 嵌套情况
Row() {
... // 10、100、500、1000 层 Row 容器嵌套
Row() {
Text('Inner Text')
}
...
}
// 平铺情况
Row() {
Row() {}
... // 10、100、500、1000 层 Row 容器并排
Text('Inner Text')
}
测试结果对比:

| 对比指标 | 10 | 100 | 500 | 1000 |
|---|---|---|---|---|
| 嵌套/层 | ||||
| 首帧绘制 | 3.2ms | 5.8ms | 17.3ms | 32ms |
| Measure | 1.88ms | 2.89ms | 5.93ms | 10.46ms |
| Layout | 0.38ms | 1.12ms | 5.26ms | 10.88ms |
| 平铺/个 | ||||
| 首帧绘制 | 3.6ms | 4.5ms | 14ms | 24.3ms |
| Measure | 2.15ms | 2.31ms | 5.61ms | 9.26ms |
| Layout | 0.39ms | 1.38ms | 4.74ms | 9.92ms |
发现没?组件平铺和嵌套在相同组件个数的情况下,性能差异不大。真正影响布局性能的因素是参与布局的节点数量。
结论:应该尽量减少整体节点数。
两个优化方向
方向一:移除冗余节点
常见冗余情况:Row 容器包含一个同样也是 Row 的子级。
// 冗余写法
Row() {
Row(){
Image()
Text()
}
Image()
}
// 优化后
Row() {
Image()
Text()
Image()
}
虽然只多了一层,但实际开发中布局往往非常复杂,冗余带来的开销很显著,尤其是在列表中动态创建组件时。
方向二:使用扁平化布局
有时候嵌套层级深但没有冗余,这时候可以切换到完全不同的布局类型来实现扁平化。
比如传统线性布局有 4 层嵌套、共 15 个节点,改成相对布局后变成 2 层嵌套、总共 10 个节点,少了 5 个节点。

主要方式:
- RelativeContainer:通过相对布局实现扁平化
- 绝对定位:通过锚点定位实现扁平化
- Grid:通过二维布局实现扁平化
五、利用布局边界减少计算
固定宽高 vs 百分比 vs 不设置
有人做了个测试,对比给容器设置固定宽高、百分比宽高、不设置宽高三种情况:
Column() {
Button("修改宽度").onClick(() => {
this.testWidth = '90%'
}).height('20%')
Row() {
// 400 条文本数据
}
}.width(this.testWidth)
测试结果:


| 对比指标/ms | 限定容器的宽高为固定值 | 未设置容器的宽高 | 限定容器的宽高为百分比 |
|---|---|---|---|
| 首帧绘制 | 60.20ms | 59.99ms | 60.50ms |
| Measure | 17.80ms | 17.76ms | 16.92ms |
| Layout | 5.5ms | 4.91ms | 4.92ms |
| 重新绘制 | 2.0ms | 38.45ms | 42.62ms |
| 重绘的 Measure | 0.50ms | 18.87ms | 20.93ms |
| 重绘的 Layout | 0.12ms | 1.41ms | 1.80ms |
发现没?首次绘制时三种情况差不多,但重新绘制时,固定宽高的性能提升明显。
为啥?
首次绘制时,无论是否设置宽高,都会对所有组件进行布局和测算。
但触发重新绘制时(比如外层容器宽度变化):
- 固定宽高:直接使用初次绘制时保留的节点大小数据,不经过重新测算
- 未设置/百分比:外层容器变化时,组件本身会触发重新 Measure,导致布局时间很长
所以对于能在初期给定宽高的组件,一定要设置固定值,性能提升很明显,尤其是组件内容复杂的情况下。
六、Scroll 嵌套 List 场景
在使用 Scroll 容器组件嵌套 List 组件加载长列表时,若不指定 List 的宽高尺寸,则默认全部加载。
不设置宽高的情况下,在进行布局时,从 FlushLayoutTask 以及 FlushRenderTask 的数据可以看到参与布局的组件数量是 100 个,设置了 List 宽高的情况下,从 FlushLayoutTask 以及 FlushRenderTask 的数据可以看到参与布局的组件数量是 12 个。
说明对于 Scroll 嵌套 List 的情况下,如果不设置 List 宽高,由于 Scroll 是可滚动容器,其高度为无穷大,List 在不设置的高度的情况下,高度也为无穷大,所以此时会创建所有的内容。而设置了高度的情况下,只会创建给定高度内的组件内容。
体现在布局时间上,没有设置宽高的情况下,总体布局时间是 32.43ms,设置了固定宽高数值的情况下,时间为 6.08ms,大幅提升了首次加载时的性能。
七、避坑指南 ⚠️
坑 1:嵌套过深
布局阶段是递归遍历所有节点的,嵌套层级过深会带来更多中间节点,导致更多计算过程。
能用扁平化布局就别一层层套娃。
坑 2:不设固定宽高
觉得"自适应"很方便?重新绘制的时候有你哭的。
能给固定值的就给固定值,别老用百分比或者不设。
坑 3:冗余嵌套
Row 套 Row、Column 套 Column,这种同方向嵌套很多都是冗余的。
用 Code Linter 扫描工具检查,重点关注 @performance/hp-arkui-remove-container-without-property 规则。
坑 4:列表项太多
列表项一多,一次性全渲染肯定卡。
咋解决?用 LazyForEach 代替 ForEach,只渲染可视区域的内容。
LazyForEach(this.listData, (item) => {
ListItem() {
// 内容
}
}, (item) => item.id)
坑 5:Scroll 嵌套 List 不设宽高
Scroll 嵌套 List 时,List 必须设置固定宽高,不然会一次性加载所有内容。
八、总结一下
影响布局性能的核心因素是参与布局的节点数量。
优化就三点:
- 移除冗余节点
- 使用扁平化布局(RelativeContainer、绝对定位、Grid)
- 给组件设置固定宽高,利用布局边界减少计算
记住啊,页面首帧和重新绘制是两个概念,固定宽高在重新绘制时优势明显。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)