1# 合理选择条件渲染和显隐控制
2
3开发者可以通过条件渲染或显隐控制两种方式来实现组件在显示和隐藏间的切换。本文从两者原理机制的区别出发,对二者适用场景分别进行说明,实现相应适用场景的示例并给出性能对比数据。
4
5## 原理机制
6
7### 条件渲染
8
9if/else条件渲染是ArkUI应用开发框架提供的渲染控制的能力之一。条件渲染可根据应用的不同状态,渲染对应分支下的UI描述。条件渲染的作用机制如下:
10
11- 页面初始构建时,会评估条件语句,构建适用分支的组件,若缺少适用分支,则不构建任何内容。
12- 应用状态变化时,会重新评估条件语句,删除不适用分支的组件,构建适用分支的组件,若缺少适用分支,则不构建任何内容。
13
14关于条件渲染的详细说明,可以参考[if/else:条件渲染](../quick-start/arkts-rendering-control-ifelse.md)。
15
16### 显隐控制
17
18显隐控制visibility是ArkUI应用开发框架提供的组件通用属性之一。开发者可以通过设定组件属性visibility不同的属性值,进而控制组件的显隐状态。visibility属性值及其描述如下:
19
20| 名称    | 描述                                     |
21| ------- | ---------------------------------------- |
22| Visible | 组件状态为可见                           |
23| Hidden  | 组件状态为不可见,但参与布局、进行占位   |
24| None    | 组件状态为不可见,不参与布局、不进行占位 |
25
26关于显隐控制的详细说明,可以参考[显隐控制](../reference/apis-arkui/arkui-ts/ts-universal-attributes-visibility.md)。
27
28### 机制区别
29
30具体针对实现组件显示和隐藏间切换的场景,条件渲染和显隐控制的作用机制区别总结如下:
31
32| 机制描述                                               | 条件渲染 | 显隐控制 |
33| ------------------------------------------------------ | -------- | -------- |
34| 页面初始构建时,若组件隐藏,组件是否会被创建           | 否       | 是       |
35| 若组件由显示变为隐藏时,组件是否会被销毁、从组件树取下 | 是       | 否       |
36| 若组件隐藏时,是否占位                                 | 否       | 可以配置 |
37
38## 适用场景
39
40通过条件渲染或显隐控制,实现组件的显示和隐藏间的切换,两者的适用场景分别如下:
41
42条件渲染的适用场景:
43
44- 在应用冷启动阶段,应用加载绘制首页时,如果组件初始不需要显示,建议使用条件渲染替代显隐控制,以减少渲染时间,加快启动速度。
45- 如果组件不会较频繁地在显示和隐藏间切换,或者大部分时间不需要显示,建议使用条件渲染替代显隐控制,以减少界面复杂度、减少嵌套层次,提升性能。
46- 如果被控制的组件所占内存庞大,开发者优先考虑内存时,建议使用条件渲染替代显隐控制,以即时销毁不需要显示的组件,节省内存。
47- 如果组件子树结构比较复杂,且反复切换条件渲染的控制分支,建议使用条件渲染配合组件复用机制,提升应用性能。
48
49显隐控制的适用场景:
50
51- 如果组件频繁地在显示和隐藏间切换时,建议使用显隐控制替代条件渲染,以避免组件的频繁创建与销毁,提升性能。
52- 如果组件隐藏后,在页面布局中,需要保持占位,建议适用显隐控制。
53
54### 显隐控制
55
56针对显示和隐藏间频繁切换的场景,下面示例通过按钮点击,实现1000张图片显示与隐藏,来简单复现该场景,并进行正反例性能数据的对比。
57
58**反例**
59
60使用条件循环实现显示和隐藏间的切换。
61
62```ts
63@Entry
64@Component
65struct WorseUseIf {
66  @State isVisible: boolean = true;
67  private data: number[] = [];
68
69  aboutToAppear() {
70    for (let i: number = 0; i < 1000; i++) {
71      this.data.push(i);
72    }
73  }
74
75  build() {
76    Column() {
77      Button("Switch visible and hidden").onClick(() => {
78        this.isVisible = !this.isVisible;
79      }).width('100%')
80      Stack() {
81        if (this.isVisible) {// 使用条件渲染切换,会频繁创建与销毁组件
82          Scroll() {
83            Column() {
84              ForEach(this.data, (item: number) => {
85                Image($r('app.media.icon')).width('25%').height('12.5%')
86              }, (item: number) => item.toString())
87            }
88          }
89        }
90      }
91    }
92  }
93}
94```
95
96**正例**
97
98使用显隐控制实现显示和隐藏间的切换。
99
100```ts
101@Entry
102@Component
103struct BetterUseVisibility {
104  @State isVisible: boolean = true;
105  private data: number[] = [];
106
107  aboutToAppear() {
108    for (let i: number = 0; i < 1000; i++) {
109      this.data.push(i);
110    }
111  }
112
113  build() {
114    Column() {
115      Button("Switch visible and hidden").onClick(() => {
116        this.isVisible = !this.isVisible;
117      }).width('100%')
118      Stack() {
119        Scroll() {
120          Column() {
121            ForEach(this.data, (item: number) => {
122              Image($r('app.media.icon')).width('25%').height('12.5%')
123            }, (item: number) => item.toString())
124          }
125        }.visibility(this.isVisible ? Visibility.Visible : Visibility.None)// 使用显隐控制切换,不会频繁创建与销毁组件
126      }
127    }
128  }
129}
130```
131
132**效果对比**
133
134正反例相同的操作步骤:通过点击按钮,将初始状态为显示的循环渲染组件切换为隐藏状态,再次点击按钮,将隐藏状态切换为显示状态。两次切换间的时间间隔长度,需保证页面渲染完成。
135
136此时组件从显示切换到隐藏状态,由于条件渲染会触发一次销毁组件,再从隐藏切换到显示,二次触发创建组件,此时用条件渲染实现切换的方式, 核心函数forEach耗时1s。
137
138![img](./figures/WorseUseIf.png)
139
140基于上例,由于显隐控制会将组件缓存到组件树,从缓存中取状态值修改,再从隐藏切换到显示,继续从缓存中取状态值修改,没有触发创建销毁组件,此时用显隐控制实现切换的方式,核心函数forEach耗时2ms。
141
142![img](./figures/BetterUseVisibility.png)
143
144可见,如果组件频繁地在显示和隐藏间切换时,使用显隐控制替代条件渲染,避免组件的频繁创建与销毁,可以提高性能。
145
146### 条件渲染
147
148针对应用冷启动,加载绘制首页时,如果组件初始不需要显示的场景,下面示例通过初始时,隐藏1000个Text组件,来简单复现该场景,并进行正反例性能数据的对比。
149
150**反例**
151
152对于首页初始时,不需要显示的组件,通过显隐控制进行隐藏。
153
154```ts
155@Entry
156@Component
157struct WorseUseVisibility {
158  @State isVisible: boolean = false; // 启动时,组件是隐藏状态
159  private data: number[] = [];
160
161  aboutToAppear() {
162    for (let i: number = 0; i < 1000; i++) {
163      this.data.push(i);
164    }
165  }
166
167  build() {
168    Column() {
169      Button("Show the Hidden on start").onClick(() => {
170        this.isVisible = !this.isVisible;
171      }).width('100%')
172      Stack() {
173        Image($r('app.media.icon')).objectFit(ImageFit.Contain).width('50%').height('50%')
174        Scroll() {
175          Column() {
176            ForEach(this.data, (item: number) => {
177              Text(`Item value: ${item}`).fontSize(20).width('100%').textAlign(TextAlign.Center)
178            }, (item: number) => item.toString())
179          }
180        }.visibility(this.isVisible ? Visibility.Visible : Visibility.None)// 使用显隐控制,启动时即使组件处于隐藏状态,也会创建
181      }
182    }
183  }
184}
185```
186
187**正例**
188
189对于首页初始时,不需要显示的组件,通过条件渲染进行隐藏。
190
191```ts
192@Entry
193@Component
194struct BetterUseIf {
195  @State isVisible: boolean = false; // 启动时,组件是隐藏状态
196  private data: number[] = [];
197
198  aboutToAppear() {
199    for (let i: number = 0; i < 1000; i++) {
200      this.data.push(i);
201    }
202  }
203
204  build() {
205    Column() {
206      Button("Show the Hidden on start").onClick(() => {
207        this.isVisible = !this.isVisible;
208      }).width('100%')
209      Stack() {
210        Image($r('app.media.icon')).objectFit(ImageFit.Contain).width('50%').height('50%')
211        if (this.isVisible) { // 使用条件渲染,启动时组件处于隐藏状态,不会创建
212          Scroll() {
213            Column() {
214              ForEach(this.data, (item: number) => {
215                Text(`Item value: ${item}`).fontSize(20).width('100%').textAlign(TextAlign.Center)
216              }, (item: number) => item.toString())
217            }
218          }
219        }
220      }
221    }
222  }
223}
224```
225
226**效果对比**
227
228正反例相同的操作步骤:通过hdc命令方式,采集应用主线程冷启动的CPU Profiler数据。具体操作,可以参考[应用性能分析工具CPU Profiler的使用指导](./application-performance-analysis.md#hdc-shell命令采集)。
229
230当应用加载绘制首页,大量组件初始不需要显示的冷启动场景时,如果组件初始不需要显示,此时使用显隐控制,启动时即使组件为隐藏状态也会创建组件。在UIAbility 启动阶段,以下为使用显隐控制的方式,渲染初始页面initialRenderView耗时401.1ms。
231
232![img](./figures/WorseUseVisibility.png)
233
234基于上例,如果组件初始不需要显示,此时使用条件渲染由于不满足渲染条件,启动时组件不会创建。在UIAbility 启动阶段,以下为使用条件渲染的方式,渲染初始页面initialRenderView耗时12.6ms。
235
236![img](./figures/BetterUseIf.png)
237
238可见,如果在应用冷启动阶段,应用加载绘制首页时,如果组件初始不需要显示,使用条件渲染替代显隐控制,可以减少渲染时间,加快启动速度。
239
240**效果对比**
241
242正反例相同的操作步骤:通过点击按钮,将初始状态为显示的Text组件切换为隐藏状态,再次点击按钮,将隐藏状态切换为显示状态。两次切换间的时间间隔长度,需保证页面渲染完成。
243
244容器内有Text组件被if条件包含,if条件结果变更会触发创建和销毁该组件,此时影响到父组件Column容器的布局,该容器内所有组件都会刷新,包括模块ForEach,因此导致主线程UI刷新耗时过长。
245
246以下为未使用容器限制条件渲染组件的刷新范围的方式,Column组件被标记脏区,ForEach耗时13ms。
247
248![img](./figures/RenderControlWithoutStack.png)
249
250基于上例,容器内有Text组件被if条件包含,if条件结果变更会触发创建和销毁该组件,此时对于这种受状态变量控制的组件,在if外套一层Stack容器,只局部刷新if条件包含的组件。因此减少了主线程UI刷新耗时。
251
252以下为使用容器限制条件渲染组件的刷新范围的方式,Column组件没有被标记脏区,没有ForEach耗时。
253
254![img](./figures/RenderControlWithStack.png)
255
256可见,如果切换项仅涉及部分组件的情况,且反复切换条件渲染的控制分支,使用条件渲染配合容器限制,精准控制组件更新的范围,可以提升应用性能。
257
258### 条件渲染和组件复用
259
260针对反复切换条件渲染的控制分支,且控制分支中的每种分支内,组件子树结构都比较复杂的场景,当有可以复用的组件情况时,可以用组件复用配合条件渲染的方式提升性能。下面示例通过定义一个自定义复杂子组件MockComplexSubBranch配合条件渲染,来展示两种场景的性能效果对比,并对该组件复用与否做正反例性能数据的对比。
261
262**反例**
263
264没有使用组件复用实现条件渲染控制分支中的复杂子组件。
265
266```ts
267@Entry
268@Component
269struct IfWithoutReusable {
270  @State isAlignStyleStart: boolean = true;
271
272  build() {
273    Column() {
274      Button("Change FlexAlign").onClick(() => {
275        this.isAlignStyleStart = !this.isAlignStyleStart;
276      })
277      Stack() {
278        if (this.isAlignStyleStart) {
279          MockComplexSubBranch({ alignStyle: FlexAlign.Start }); // 未使用组件复用机制实现的MockComplexSubBranch
280        } else {
281          MockComplexSubBranch({ alignStyle: FlexAlign.End });
282        }
283      }
284    }
285  }
286}
287```
288
289其中MockComplexSubBranch是由3个Flex容器组件分别弹性布局200个Text组件构造而成,用以模拟组件复杂的子树结构,代码如下:
290
291```ts
292@Component
293export struct MockComplexSubBranch {
294  @State alignStyle: FlexAlign = FlexAlign.Center;
295
296  build() {
297    Column() {
298      Column({ space: 5 }) {
299        Text('ComplexSubBranch not reusable').fontSize(9).fontColor(0xCCCCCC).width('90%')
300        AlignContentFlex({ alignStyle: this.alignStyle });
301        AlignContentFlex({ alignStyle: this.alignStyle });
302        AlignContentFlex({ alignStyle: this.alignStyle });
303      }
304    }
305  }
306}
307
308@Component
309struct AlignContentFlex {
310  @Link alignStyle: FlexAlign;
311  private data: number[] = [];
312
313  aboutToAppear() {
314    for (let i: number = 0; i < 200; i++) {
315      this.data.push(i);
316    }
317  }
318
319  build() {
320    Flex({ wrap: FlexWrap.Wrap, alignContent: this.alignStyle }) {
321      ForEach(this.data, (item: number) => {
322        Text(`${item % 10}`).width('5%').height(20).backgroundColor(item % 2 === 0 ? 0xF5DEB3 : 0xD2B48C)
323      }, (item: number) => item.toString())
324    }.size({ width: '100%', height: 240 }).padding(10).backgroundColor(0xAFEEEE)
325  }
326}
327```
328
329**正例**
330
331使用组件复用实现条件渲染控制分支中的复杂子组件。
332
333```ts
334@Entry
335@Component
336struct IfWithReusable {
337  @State isAlignStyleStart: boolean = true;
338
339  build() {
340    Column() {
341      Button("Change FlexAlign").onClick(() => {
342        this.isAlignStyleStart = !this.isAlignStyleStart;
343      })
344      Stack() {
345        if (this.isAlignStyleStart) {
346          MockComplexSubBranch({ alignStyle: FlexAlign.Start }); // 使用组件复用机制实现的MockComplexSubBranch
347        } else {
348          MockComplexSubBranch({ alignStyle: FlexAlign.End });
349        }
350      }
351    }
352  }
353}
354```
355
356其中MockComplexSubBranch实现如下方所示,AlignContentFlex 代码一致,此处不再赘述。
357
358```ts
359@Component
360@Reusable // 添加Reusable装饰器,声明组件具备可复用的能力
361export struct MockComplexSubBranch {
362  @State alignStyle: FlexAlign = FlexAlign.Center;
363
364  aboutToReuse(params: ESObject) { // 从缓存复用组件前,更新组件的状态变量
365    this.alignStyle = params.alignStyle;
366  }
367
368  build() {
369    Column() {
370      Column({ space: 5 }) {
371        Text('ComplexSubBranch reusable').fontSize(9).fontColor(0xCCCCCC).width('90%')
372        AlignContentFlex({ alignStyle: this.alignStyle });
373        AlignContentFlex({ alignStyle: this.alignStyle });
374        AlignContentFlex({ alignStyle: this.alignStyle });
375      }
376    }
377  }
378}
379
380```
381
382**效果对比**
383
384正反例相同的操作步骤:通过点击按钮,Text组件会在Flex容器主轴上,由首端对齐转换为尾端对齐,再次点击按钮,由尾端对齐转换为首端对齐。两次切换间的时间间隔长度,需保证页面渲染完成。
385
386此时由于按钮反复切换了条件渲染分支,且每一分支中的MockComplexSubBranch组件子树结构都比较复杂,会造成大量的组件销毁创建过程,以下为不使用组件复用实现条件渲染控制分支中的子组件的方式,应用Index主页面渲染耗时180ms。
387
388![img](./figures/IfWithoutReusable.png)
389
390基于上例,考虑到将控制分支中的复杂组件子树结构在父组件中进行组件复用,此时从组件树缓存中拿出子组件,避免大量的组件销毁创建过程,以下为使用组件复用实现条件渲染控制分支中的子组件的方式,应用Index主页面渲染耗时14ms。
391
392![img](./figures/IfWithReusable.png)
393
394可见,针对反复切换条件渲染的控制分支的情况,且控制分支中的组件子树结构比较复杂,使用组件复用机制,可以提升应用性能。