应用启动到UI页面展示过程包含框架初始化、页面加载和布局渲染三个步骤。其中页面加载和布局渲染的主要流程如下:

可以看到,应用启动后页面加载和渲染的性能与FrameNode树上的节点数量以及每个节点上的属性相关。因此,为缩短页面加载和布局渲染时长,在前端使用UI组件时可以考虑以下优化方案:

如上图所示,自定义组件创建完成之后,在build函数执行之前,将先执行aboutToAppear()生命周期回调函数。此时若在该函数中执行耗时操作,将阻塞UI渲染,增加UI主线程负担。因此,应尽量避免在自定义组件的生命周期内执行高耗时操作。对于复杂计算的耗时场景,可以将计算结果进行缓存处理。对于不需要等待结果的高耗时任务,可以采用多线程处理该任务,通过并发的方式避免主线程阻塞。在aboutToAppear()生命周期函数内建议只做当前组件的初始化逻辑,其他业务逻辑可以按需提前或延后处理。假设在首页视频列表中的子组件内需要初始化创建一个复杂播放器对象,该对象的创建非常耗时。若在该组件的aboutToAppear()函数中创建该对象,当首页加载渲染时,列表内每个子组件的渲染都将等待相应的播放器对象初始化创建完成,此时页面加载将非常耗时甚至可能出现白屏。伪代码如下:
反例
@Component
export struct VideoCard {
// ...
aboutToAppear(): void {
// Create a complex object task, if the task takes 1s to execute, the component will be rendered again after 1s
this.createComplexVideoPlayer();
}
// ...
}
@Component
export struct CardList {
@State videoList: VideoItem[] = getVideoList();
build() {
List() {
ForEach(this.videoList, (item: VideoItem) => {
ListItem() {
VideoCard({ item })
}
}, (item: VideoItem) => item.id)
}
}
} 对于该场景,可以考虑将创建播放器对象任务的时机延后。如,计算当前组件出现在页面中的位置,当子组件滑动到页面的三分之一处时再创建播放器对象并播放视频。此时,页面首次渲染时,不会出现主线程阻塞。示例代码如下:
正例
@Component
export struct VideoCard {
@State isVideoInit: boolean = false;
// ...
build() {
Column() {
// Video Playback Component
}
.onAreaChange((old, newValue) => {
if (!this.isVideoInit) {
let positionY: number = newValue.position.y as number
if (positionY < screenHeight / 3) {
this.createComplexVideoPlayer();
this.isVideoInit = true;
}
}
})
}
// ...
}
@Component
export struct CardList {
@State videoList: VideoItem[] = getVideoList();
build() {
List() {
ForEach(this.videoList, (item: VideoItem) => {
ListItem() {
VideoCard({ item })
}
}, (item: VideoItem) => item.id)
}
}
} 例如在生命周期aboutToAppear中应该避免使用@ohos.resourceManager (资源管理)的getXXXSync()接口入参中直接使用资源信息,推荐使用资源id作为入参,推荐用法为:resourceManager.getStringSync($r('app.string.test').id)。下面以getStringSync为例,测试一下这两种参数在方法中的使用是否会有耗时区别。
反例
import { hilog, hiTraceMeter } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct Index {
@State message: string = 'getStringSync';
private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
aboutToAppear(): void {
hiTraceMeter.startTrace('getStringSync', 1);
// The input parameter of the getStringSync interface uses the resource directly, without using the resource ID.
try {
this.context.resourceManager.getStringSync($r('app.string.app_name'));
} catch (error) {
let err = error as BusinessError;
hilog.warn(0x000, 'testTag', `getStringSync failed, code=${err.code}, message=${err.message}`);
}
hiTraceMeter.finishTrace('getStringSync', 1);
}
build() {
RelativeContainer() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.height('100%')
.width('100%')
}
} 可以通过冷启动分析:Launch分析工具抓取Trace,根据hiTraceMeter性能打点,查看耗时为1.956ms。

正例
import { hilog, hiTraceMeter } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct Index {
private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
@State message: string = 'getStringSyncAfter';
aboutToAppear(): void {
hiTraceMeter.startTrace('getStringSyncAfter', 2);
// The input parameter of the getStringSync interface uses the resource ID.
try {
this.context.resourceManager.getStringSync($r('app.string.app_name').id);
} catch (error) {
let err = error as BusinessError;
hilog.warn(0x000, 'testTag', `getStringSync failed, code=${err.code}, message=${err.message}`);
}
hiTraceMeter.finishTrace('getStringSyncAfter', 2);
}
build() {
RelativeContainer() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.height('100%')
.width('100%')
}
} 可以通过冷启动分析:Launch分析工具抓取Trace,根据hiTraceMeter性能打点,查看耗时为0.071ms。

写法 | 耗时情况 |
|---|---|
资源信息为参数:getStringSync($r('app.string.app_name')) | 1.956ms |
资源ID为参数:getStringSync($r('app.string.app_name').id) | 0.071ms |
可得出结论:参数为资源信息时比参数为资源ID值时耗时更多。所以当需要使用类似方法时,使用资源ID值作为参数更优,可有效减少自定义组件生命周期耗时。
在使用组件开发应用UI界面时,会为每个组件设置属性,进行UI样式、行为等逻辑处理。当应用中单个组件设置了大量属性且该组件在应用中被大量使用时,单个组件的设置对应用的整体性能会产生较大影响。比如,在RN框架开发中,单个组件需要设置21个属性,且该组件在ForEach循环中使用。在该场景下,由于不知道应用实际需要使用哪些属性,因此把所有的属性通过属性方法的方式设置到组件上。而在实际使用中,大部分应用只会用到其中很少的几个属性,其他属性均维持默认值,这导致了大量属性的冗余设置。该场景示例代码片段如下:
build() {
Stack() {
this.renderChildren()
}
.width(this.descriptor.layoutMetrics.frame.size.width)
.height(this.descriptor.layoutMetrics.frame.size.height)
.backgroundColor(convertColorSegmentsToString(this.descriptor.props.backgroundColor))
.position({ y: this.descriptor.layoutMetrics.frame.origin.y, x: this.descriptor.layoutMetrics.frame.origin.x })
.borderWidth(this.descriptor.props.borderWidth)
.borderColor({
left: convertColorSegmentsToString(this.descriptor.props.borderColor.left),
top: convertColorSegmentsToString(this.descriptor.props.borderColor.top),
right: convertColorSegmentsToString(this.descriptor.props.borderColor.right),
bottom: convertColorSegmentsToString(this.descriptor.props.borderColor.bottom)
})
.borderRadius(this.descriptor.props.borderRadius)
.borderStyle(this.getBorderStyle())
.opacity(this.getOpacity())
.transform(this.descriptor.props.transform != undefined ? convertMatrixArrayToMatrix4(this.descriptor.props.transform) : undefined)
.clip(this.getClip())
.hitTestBehavior(this.getHitTestMode())
.shadow(this.getShadow())
} 从该场景中可以看到,在应用开发中,当注册了大量冗余属性的组件需要在视图上批量展示时对性能有较大影响。此时,可以考虑采用动态属性设置动态注册组件属性的方式,替换使用属性方法静态注册组件属性的方式。
使用AttributeModifier动态注册组件属性相比于直接在组件上使用属性方法静态注册组件属性,主要存在以下两点区别:
以一个简单的公共头像组件为例演示AttributeModifier方案的性能收益。公共头像组件要求对于有用户头像的数据,界面展示用户头像图片。对于没有用户头像的数据,界面展示灰色背景和用户名的第一个字符。现列表展示1000个头像组件,界面效果如下:

使用属性方法的方式给头像组件设置属性,代码如下:
import { util } from '@kit.ArkTS';
@Observed
class User {
id: string;
name: string;
avatarImage: ResourceStr;
constructor(id: string, name: string, avatarImage: ResourceStr) {
this.id = id;
this.name = name;
this.avatarImage = avatarImage;
}
}
//create data
const DEFAULT_BACKGROUND_COLOR = Color.Grey;
const getUsers = () => {
return Array.from(Array(1000), (item: User, i: number) => {
return new User(
util.generateRandomUUID(),
i % 2 === 0 ? '张三' : '李四',
i % 2 === 0 ? '' : $r('app.media.startIcon')
);
});
}
@Component
export struct AvatarGrid {
@State users: User[] = getUsers();
build() {
Grid() {
ForEach(this.users, (u: User) => {
GridItem() {
Avatar({ user: u })
}
}, (user: User) => user.id)
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
.columnsGap(4)
.rowsGap(4)
}
}
// Avatar component
@Component
struct Avatar {
@ObjectLink user: User;
build() {
Row() {
if (!this.user.avatarImage) {
Text(this.user.name.charAt(0))
.fontSize(28)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
}
}
.backgroundImage(this.user.avatarImage)
.backgroundImageSize(ImageSize.Cover)
.backgroundColor(DEFAULT_BACKGROUND_COLOR)
.justifyContent(FlexAlign.Center)
.size({ width: 50, height: 50 })
.borderRadius(25)
// .padding(2)
// .margin(2)
// .opacity(1)
// .clip(false)
// .layoutWeight(1)
// .backgroundBlurStyle(BlurStyle.NONE)
// .alignItems(VerticalAlign.Center)
// .borderWidth(1)
// .borderColor(Color.Pink)
// .borderStyle(BorderStyle.Solid)
// .expandSafeArea([SafeAreaType.SYSTEM])
// .rotate({angle: 5})
// .responseRegion({x: 0})
// .mouseResponseRegion({x: 0})
// .constraintSize({minWidth: 25})
// .hitTestBehavior(HitTestMode.Default)
// .backgroundImagePosition(Alignment.Center)
// .foregroundBlurStyle(BlurStyle.NONE)
}
} 将方案改为采用AttributeModifier动态注册属性的方式,需要新增自定义类实现AttributeModifier接口,并修改Avatar组件的属性注册逻辑。具体改动代码如下:
// 1.Custom Attribute Modifier, this class implements the AttributeModifier interface
class RowModifier implements AttributeModifier<RowAttribute> {
private customImage: ResourceStr = '';
private static instance: RowModifier;
constructor() {
}
setCustomImage(customImage: ResourceStr) {
this.customImage = customImage;
return this;
}
// Adopting a singleton pattern avoids creating a new modifier for each component, increasing the performance overhead incurred by creating the
public static getInstance(): RowModifier {
if (!RowModifier.instance) {
RowModifier.instance = new RowModifier();
}
return RowModifier.instance;
}
// 2.Implement the applyNormalAttribute method of the AttributeModifier interface to customize the logic of attribute setting.
applyNormalAttribute(instance: RowAttribute) {
if (this.customImage) {
instance.backgroundImage(this.customImage);
instance.backgroundImageSize(ImageSize.Cover);
} else {
instance.backgroundColor(Color.Blue);
instance.justifyContent(FlexAlign.Center);
// instance.padding(2)
// instance.margin(2)
// instance.opacity(1)
// instance.clip(false)
// instance.layoutWeight(1)
// instance.backgroundBlurStyle(BlurStyle.NONE)
// instance.alignItems(VerticalAlign.Center)
// instance.borderWidth(1)
// instance.borderColor(Color.Pink)
// instance.borderStyle(BorderStyle.Solid)
// instance.expandSafeArea([SafeAreaType.SYSTEM])
// instance.rotate({ angle: 5 })
// instance.responseRegion({x: 0})
//instance.mouseResponseRegion({x: 0})
// instance.constraintSize({minWidth: 25})
// instance.hitTestBehavior(HitTestMode.Default)
//instance.backgroundImagePosition(Alignment.Center)
//instance.foregroundBlurStyle(BlurStyle.NONE)
}
instance.size({ width: 50, height: 50 });
instance.borderRadius(25);
}
}
@Component
struct Avatar {
@ObjectLink user: User;
build() {
Row() {
if (!this.user.avatarImage) {
Text(this.user.name.charAt(0))
.fontSize(28)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
}
}
// 3.Pass a custom RowModifier class as a parameter to enable on-demand property registration
.attributeModifier(RowModifier.getInstance().setCustomImage(this.user.avatarImage))
}
} 对于上述两种方案,逐渐增加注册的属性个数,通过DevEco Studio提供的Launch场景分析能力获取两个方案的页面加载耗时(PageRouterManager::LoadPage)和应用侧首帧耗时(First Frame - App Phase),对比如下:
静态注册属性 | 动态注册属性 | |||||||
|---|---|---|---|---|---|---|---|---|
注册的属性个数 | 6个 | 12个 | 18个 | 24个 | 6个 | 12个 | 18个 | 24个 |
PageRouterManager::LoadPage | 639ms900μs | 668ms624μs | 719ms139μs | 764ms437μs | 640ms989μs | 662ms112μs | 705ms407μs | 717ms294μs |
First Frame - App Phase | 45ms554μs | 45ms638μs | 52ms918μs | 52ms643μs | 44ms603μs | 43ms923μs | 46ms709μs | 46ms355μs |


可以看到,当注册的属性个数较少时,使用动态注册的方案收益并不明显。当注册的属性个数递增时,动态注册的收益效果同步线性递增。
在ArkUI中使用自定义组件时,在build阶段将在后端FrameNode树创建一个相应的CustomNode节点,在渲染阶段时也会创建对应的RenderNode节点,如下图所示。

因此,在应用开发时,减少自定义组件的使用,尤其是自定义组件在循环中的使用,将成倍减少FrameNode节点树上CustomNode节点数量,有效缩短页面的加载和渲染时长。当在应用中使用自定义组件时,可以优先考虑使用@Builder函数代替自定义组件,@Builder函数不会在后端FrameNode节点树上创建一个新的树节点。
例如,在使用ForEach循环展示卡片列表信息时,界面展示如下:

使用自定义组件方案,示例代码如下:
import { util } from '@kit.ArkTS';
interface User {
id: string;
name: string;
age?: number;
avatarImage?: ResourceStr;
//introduction: string;
// ...
}
// create data
const DEFAULT_BACKGROUND_COLOR = Color.Pink;
const getUsers = () => {
const USERS: User[] = [{
id: '1',
name: '张三',
}, {
id: '2',
name: '李四',
}, {
id: '3',
name: '王五',
}];
return Array.from(Array(30), (item: User, i: number) => {
return {
id: util.generateRandomUUID(),
name: USERS[i%3].name,
avatarImage: $r('app.media.startIcon'),
age: 18 + i
} as User;
});
}
// User Card List Component
@Component
export struct UserCardList {
@State users: User[] = getUsers();
build() {
List({ space: 8 }) {
ForEach(this.users, (item: User) => {
ListItem() {
UserCard({ name: item.name, age: item.age, avatarImage: item.avatarImage })
}
}, (item: User) => item.id)
}
.alignListItem(ListItemAlign.Center)
}
}
// User Card Customization Component
@Component
struct UserCard {
@Prop avatarImage: ResourceStr;
@Prop name: string;
@Prop age: number;
build() {
Row() {
Row() {
Image(this.avatarImage)
.size({ width: 50, height: 50 })
.borderRadius(25)
.margin(8)
Text(this.name)
.fontSize(30)
}
Text(`age:${this.age.toString()}`)
.fontSize(20)
}
.backgroundColor(DEFAULT_BACKGROUND_COLOR)
.justifyContent(FlexAlign.SpaceBetween)
.borderRadius(8)
.padding(8)
.height(66)
.width('80%')
}
} 改用@Builder函数的方式代替自定义组件UserCard的方案,具体修改的代码如下:
// 1. Customizing @Builder Function Components
@Builder
function UserCardBuilder(name: string, age?: number, avatarImage?: ResourceStr) {
Row() {
Row() {
Image(avatarImage)
.size({ width: 50, height: 50 })
.borderRadius(25)
.margin(8)
Text(name)
.fontSize(30)
}
Text(`age:${age?.toString()}`)
.fontSize(20)
}
.backgroundColor(Color.Blue)
.justifyContent(FlexAlign.SpaceBetween)
.borderRadius(8)
.padding(8)
.height(66)
.width('80%')
}
@Component
export struct UserCardList {
@State users: User[] = getUsers();
build() {
List({ space: 8 }) {
ForEach(this.users, (item: User) => {
ListItem() {
// 2. Using the @Builder function in a build function
UserCardBuilder(item.name, item.age, item.avatarImage)
}
}, (item: User) => item.id)
}
.alignListItem(ListItemAlign.Center)
}
} 将组件数量从30个递增到3000个,通过profiler获取页面加载标签PageRouterManager::LoadPage和页面UI刷新任务标签UITaskScheduler::FlushTask的耗时,对比两种方案的耗时如下:


通过对比图可以看到,@Builder方案在页面加载和刷新UI页面(包括布局、渲染和动画)方面优于自定义组件方案。随着组件个数增加,收益也线性增加。
对于需要展示大量组件的场景,通常会使用布局容器组件,以达到快速实现页面布局的需求。在使用布局容器组件时,由于一次需要展示多个组件,可能出现首帧耗时过长甚至掉帧问题。此时可以考虑对容器组件内的子组件进行按需加载或懒加载等处理,对于相同结构的组件也可以使用组件复用能力。针对每个布局容器组件的性能优化可以参考布局优化指导。