概览:本文深入解析渲染丢帧的成因,通过渲染范围控制、布局节点精简、组件绘制优化、状态刷新控制、动画帧率提升、视觉流畅感知六大方向,系统介绍保障界面流畅的关键技术。并通过“高负载场景的分帧渲染(displaySync)”和“长列表渲染优化(LazyForEach)”两个案例,具体演示如何解决复杂UI导致的丢帧问题。
什么是渲染丢帧?
帧:构成图像或视频的基本单位,屏幕默认会以120帧/秒进行渲染内容,120也叫作帧率。
丢帧:渲染过程中应该呈现的帧未按时呈现,从而跳过1个或多个帧,这种现象称为丢帧。
产生丢帧的因素
CPU:负责准备要渲染的数据,处理渲染对象的位置、状态等,传递GPU。
GPU:接收CPU数据,进行复杂的图形计算,生成屏幕显示的像素数据。
内存:存储CPU与GPU之间交换的数据,以便程序能快速访问和处理。
渲染丢帧优化的方向
1、渲染范围控制:要看什么再渲染什么,反复看的别着急销毁。
1.合理控制显示隐藏:如果使用if会进行频繁的创建和销毁,CPU进行处理。如果使用visibility只需要GPU处理,CPU释放出来了。
Text('频繁的显示和隐藏内容')
.visibility(this.visible ? Visibility.Visible : Visibility.None)
2.组件复用:增加@Reusable,原理同上。
@Reusable
@Component
export struct RedText {
}
3.懒加载(案例:长列表渲染优化):只渲染页面看见的组件,看不见的不渲染。
List(){
LazyForEach(this.list,()=>{
ListItem(){
Text('长列表循环项组件')
}
})
}
4.分帧渲染(案例:高负载场景的分帧渲染):在第一帧加载所有数据压力比较大,进行分帧渲染。
let count = 0 // 当前是第几帧
this.displayAbi.on('frame', () => {
if (count == 0) {
}
count++
})
2、布局节点减少:减少UI布局的节点状态创建和计算的时间
1.自定义构建函数代替自定义组件:自定义构建函数(@builder)是无状态组件更轻量级,可以替换自定义组件(@Component)。
2.合理使用容器组件:耗时 Column/Row < Stack < Flex < Grid/GridItem。
3.精简节点数,减少嵌套层数:嵌套会更浪费性能。
3、组件绘制优化:减少绘制的负担
1.避免自定义组件的生命周期内执行高耗操作
aboutToAppear(): void {
// 在绘制组件前,避免耗时操作阻塞UI渲染
}
2.按需注册组件属性:一个联系人列表,是图时才用的到backgroundImage、backgroundImageSize,是字时才用的到backgroundColor、justifyContent,size和borderRadius两种都用得到。
这时没必要都渲染,使用动态注册属性优化。
Row() {
}
.backgroundImage('') // 图
.backgroundImageSize(ImageSize.Contain) // 图
.backgroundColor(Color.Pink) // 字
.justifyContent(FlexAlign.Center) // 字
.size({ width: 36, height: 36 }) // 图+字
.borderRadius(18) // 图+字
// 动态注册属性
Row() {
Text('666')
}
.attributeModifier(RowModifier.getInstance().setCustomImage(Date.now() % 2 === 1 ? $r('app.media.startIcon') : ''))
// 自定义类实现动态注册属性
class RowModifier implements AttributeModifier<RowAttribute> {
private customImage: ResourceStr = '';
private static instance: RowModifier;
constructor() {}
setCustomImage(customImage: ResourceStr) {
this.customImage = customImage;
return this;
}
public static getInstance(): RowModifier {
if (!RowModifier.instance) {
RowModifier.instance = new RowModifier();
}
return RowModifier.instance;
}
applyNormalAttribute(instance: RowAttribute){
instance.size({ width: 50, height: 50 });
instance.borderRadius(25);
if (this.customImage) {
instance.backgroundImage(this.customImage);
instance.backgroundImageSize(ImageSize.Cover);
} else {
instance.backgroundColor(Color.Blue);
instance.justifyContent(FlexAlign.Center);
}
}
}
3.减少布局计算:耗时 限定容器宽高为固定值 < 未设置容器宽高 < 限定容器的宽高为百分比
4、状态刷新控制:减轻变化产生的影响范围
1.避免不必要的状态变量使用
2.最小化状态共享范围
@State + @Prop:会进行一次深拷贝对内存进行消耗。
@State + @Link:共享同一地址,内存消耗低一点。
3.减少不必要的层层传递
4.精细化拆分复杂状态
// c不影响UI但还会触发页面更新
@Observed
class ClassA {
a: string = ''; // 影响UI
b: number = 0; // 影响UI
c: boolean = true; // 不影响UI
}
// 将影响UI刷新的拆分出去,c就不引起页面更新了
@Observed
class ClassB {
x: ClassC = new ClassC();
c: boolean = true;
}
@Observed
class ClassC {
a: string = '';
b: number = 0;
}
// 如果存在加@Track的,不加@Track的c就不引起页面更新了
@Observed
class ClassD {
@Track a: string = '';
@Track b: number = 0;
c: boolean = true;
}
5.监听和订阅精准控制组件刷新
// 监听
@Link @Watch('countUpdate') count: number
@State color: Color = Math.random() > 0.5 ? Color.Red : Color.Blue
countUpdate() {
this.color = this.count % 2 === 0 ? Color.Red : Color.Blue
}
// 订阅
emitter.on(event, callback);
emitter.emit(event, eventData);
2.5 动画帧率优化
1.使用系统的动画接口
animation:都是进行大量优化后的
2.使用图形变换属性实现组件变化
图形变换属性 布局属性
rotate /
translate position、offset
scale width、height、Size
transform /
width、height等布局属性的改变,是需要CPU重新计算的。而图形变换属性只有GPU计算要绘制的内容就可以了
3.合理使用animateTo
4.使用renderGroup缓存动效
页面有很多地方要进行一个动画,动画要立刻渲染出来压力就会很大了,可能导致丢帧。这时使用renderGroup缓存动画效果,就可以做到离屏渲染了。
Column() {
}
.动画
.renderGroup(true)
2.6 感知流畅优化
1.视觉感知优化:骨架屏、首页加载。
2.转场场景动效感知流畅
3.合理动画时长使应用感知流畅:如长时加载使用百分比进度条,短时使用环形加载。
案例:高负载场景的分帧渲染(displaySync)
1.创建display实例对象
displayAbi: displaySync.DisplaySync | null = null
this.displayAbi = displaySync.create()
2.设置可变帧率
this.displayAbi.setExpectedFrameRateRange({
expected: 60, // 期待帧率
min: 60, // 最小帧率
max: 120 // 最大帧率
})
3.监听帧率变化(根据自己的项目测试渲染多少不丢帧)
let count = 0 // 当前是第几帧
this.displayAbi.on('frame', () => {
if (count == 0) {
}
count++
})
4.开启监听
this.displayAbi.start()
5.关闭和取消
this.displayAbi?.stop()
this.displayAbi?.off('frame')
6.希望组件分帧加载,加上lazy
import lazy { MyStudy } from '../components/MyStudy'
@State showStudy: boolean = false
if (this.showStudy) {
MyStudy({ studyList: this.studyList })
}
module.json5
"requestPermissions": [{
"name": "ohos.permission.INTERNET"
}]
ets > pages > index.ets
import { ExploreItemData, getActivityListAPI, getStudyListAPI, getSwiperListAPI, SwiperItemData } from '../apis'
import lazy { MyStudy } from '../components/MyStudy'
import lazy { MyActivity } from '../components/MyActivity'
import { displaySync } from '@kit.ArkGraphics2D'
import { LazyForEachType } from '../utils/LazyForEachUtil'
@Entry
@Component
struct Index {
@State swiperList: SwiperItemData[] = []
@State studyList: ExploreItemData[] = []
// @State activityList: ExploreItemData[] = []
@State activityList: LazyForEachType<ExploreItemData> = new LazyForEachType()
displayAbi: displaySync.DisplaySync | null = null
@State showStudy: boolean = false
@State showActivity: boolean = false
async aboutToAppear() {
const res = await Promise.allSettled([getSwiperListAPI(), getStudyListAPI(), getActivityListAPI()])
this.displayAbi = displaySync.create()
this.displayAbi.setExpectedFrameRateRange({
expected: 60,
min: 60,
max: 120
})
let count = 0
this.displayAbi.on('frame', () => {
if (count == 0) { // 第0帧:渲染静态内容
} else if (count == 1) { // 第1帧:渲染轮播图,只渲染第一张,剩下的不急(轮播图共3张)
if (res[0].status == 'fulfilled') {
this.swiperList.push(...res[0].value.splice(0, 1))
}
} else if (count == 2) { // 第2帧:渲染学习静态内容
this.showStudy = true // 改为true开始渲染学习静态内容,不渲染数据(此时没有数据)
} else if (count == 3) { // 第3帧:渲染学习前两张(学习数据共4张)
if (res[1].status == 'fulfilled') {
this.studyList.push(...res[1].value.splice(0, 2))
}
} else if (count == 4) { // 第4帧:渲染轮播图剩下两张
if (res[0].status == 'fulfilled') {
this.swiperList.push(...res[0].value.splice(0, 2))
}
} else if (count == 5) { // 第5帧:渲染学习剩下两张
if (res[1].status == 'fulfilled') {
this.studyList.push(...res[1].value.splice(0, 2))
}
} else if (count == 6) { // 第6帧:渲染活动静态内容
this.showActivity = true // 改为true开始渲染活动静态内容,不渲染数据(此时没有数据)
} else if (count == 7) { // 第7帧:渲染活动的两条数据
if (res[2].status == 'fulfilled') {
// this.activityList.push(...res[2].value.splice(0,2)) // 添加数据
this.activityList.pushData(res[2].value.splice(0, 2)) // 懒加载添加数据
}
} else if (count == 8) { // 第8帧:渲染活动的三条数据
if (res[2].status == 'fulfilled') {
// this.activityList.push(...res[2].value.splice(0,3))
this.activityList.pushData(res[2].value.splice(0, 3))
}
} else if (count == 9) { // 第9帧:渲染活动的三条数据
if (res[2].status == 'fulfilled') {
// this.activityList.push(...res[2].value.splice(0,3))
this.activityList.pushData(res[2].value.splice(0, 3))
}
this.displayAbi?.stop()
this.displayAbi?.off('frame')
}
count++
})
this.displayAbi.start()
}
build() {
RelativeContainer() {
// 顶部
Row() {
Text('DEVELOPER')
.fontSize(18)
Row() {
SymbolGlyph($r('sys.symbol.person_crop_circle_fill_1'))
.fontColor(["#9A9A9A"])
.fontSize(36)
}
}
.width('100%')
.backgroundColor("#f5f7f8")
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 16, right: 16 })
.height(60)
.id('my_nav')
Scroll() {
Column() {
// 1.轮播图
Swiper() {
ForEach(this.swiperList, (item: SwiperItemData) => {
Column({ space: 16 }) {
Text(item.title)
.fontWeight(FontWeight.Bold)
.fontSize(24)
.fontColor(item.isDark ? Color.White : Color.Black)
Text(item.subTitle)
.fontSize(12)
.fontColor(item.isDark ? Color.White : Color.Black)
}
.backgroundImage(item.url)
.backgroundImageSize(ImageSize.Cover)
.justifyContent(FlexAlign.Center)
})
}
.width('100%')
.height(180)
.autoPlay(true)
// 2.学习路径列表(条件渲染)
if (this.showStudy) {
MyStudy({ studyList: this.studyList })
}
// 3.活动列表(条件渲染)
if (this.showActivity) {
MyActivity({ activityList: this.activityList })
}
}
}
.layoutWeight(1)
.backgroundColor(Color.White)
.scrollBar(BarState.Off)
.alignRules({
top: {
anchor: "my_nav",
align: VerticalAlign.Bottom
}
})
}
}
}
案例:长列表渲染优化(LazyForEach)
ets > utils > LazyForEachUtil.ets
export class LazyForEachType<T> implements IDataSource {
listeners: DataChangeListener[] = []
data: T[] = [] // 数据
totalCount(): number {
return this.data.length
}
getData(index: number): T {
return this.data[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) === -1) {
this.listeners.push(listener) // 如果没有监听器,添加监听器
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this.listeners.indexOf(listener)
if (index > -1) {
this.listeners.splice(index, 1) // 移除监听器
}
}
pushData(list: T[]) {
this.data.push(...list)
this.listeners.forEach(listener => { // 拿到每一个监听器
listener.onDataReloaded() // 重载数据
})
}
}
ets > components > MyActivity.ets
import { ExploreItemData, getActivityListAPI } from '../apis'
import { LazyForEachType } from '../utils/LazyForEachUtil'
@Component
export struct MyActivity {
// @Link activityList:ExploreItemData[]
@Link activityList: LazyForEachType<ExploreItemData>
build() {
List({ space: 8 }) {
// ForEach(this.activityList, (item: ExploreItemData) => {
LazyForEach(this.activityList, (item: ExploreItemData) => {
ListItem() {
Row({ space: 20 }) {
Column({ space: 20 }) {
Text(item.title)
.fontWeight(FontWeight.Bold)
Text(item.subTitle)
.fontSize(12)
.fontColor(Color.Gray)
.maxLines(2)
.textOverflow({
overflow: TextOverflow.Ellipsis
})
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Image(item.url)
.width(100)
.borderRadius(8)
}
.width('100%')
.height(100)
.padding(12)
.borderRadius(8)
.border({
width: {
bottom: 1
},
color: "#f5f7f8"
})
}
.height(100)
})
}
.width('100%')
.height('100%')
.padding({ bottom: 24 })
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
.onReachEnd(async () => {
// 防止第一次加载时出发
// if(this.activityList.length==0)return
if (this.activityList.totalCount() == 0) {
return
}
const res = await getActivityListAPI()
// this.activityList = [...this.activityList,...res]
this.activityList.pushData(res)
})
}
}
ets > components > MyStudy.ets
import { ExploreItemData } from '../apis'
@Component
export struct MyStudy {
@Link studyList: ExploreItemData[]
build() {
Column({ space: 8 }) {
Row() {
Text('探索你的学习路径')
.fontSize(26)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height(60)
.justifyContent(FlexAlign.Center)
ForEach(this.studyList, (item: ExploreItemData) => {
Column({ space: 8 }) {
Text(item.title)
.fontWeight(FontWeight.Bold)
.fontSize(28)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
Text(item.subTitle)
.fontSize(12)
.fontColor(Color.Gray)
.textAlign(TextAlign.Center)
}
.backgroundImage(item.url)
.backgroundImageSize(ImageSize.Cover)
.width('100%')
.aspectRatio(15 / 11)
.padding({ top: 32, left: 50, right: 50 })
})
Row() {
Text('更多精彩活动')
.fontSize(26)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height(60)
.justifyContent(FlexAlign.Center)
}
}
}
ets > apis > index.ets(模拟接口,接口没有耗时(耗时忽略),关注渲染能力)
export interface SwiperItemData {
url: string
title: string
subTitle: string
isDark: boolean
}
export interface ExploreItemData {
url: string
title: string
subTitle: string
}
const swiperList:SwiperItemData[] = [
{
url: 'https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/0417/shouye/chuangxinsai600.jpg',
title: 'HarmonyOS 创新赛',
subTitle: '鸿蒙生态最大规模开发者官方赛事,最高获百万激励。',
isDark: true
},
{
url: 'https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/jilijihua2025-1920-0812.jpg',
title: '鸿蒙应用开发者激励计划2025',
subTitle: '总奖金超亿元,每款应用最高可获取10000元激励。',
isDark: false
},
{
url: 'https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/xinnengliyilan-5300x932.jpg',
title: 'HarmonyOS 6 新能力一览',
subTitle: '在开发中专注创造,让体验无缝流转。',
isDark: true
}
]
const studyList:ExploreItemData[] = [
{
url: 'https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/shouye/2025-07-10/600-kaifazhexuetang-0805.jpeg',
title: '开发者学堂',
subTitle: '学、练、考、证全流程服务,让你快速成为 HarmonyOS 人才。',
},
{
url: 'https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/shouye/2025-07-10/600-kaifazheshequ-0805.jpeg',
title: '开发者社区',
subTitle: '提出你的问题,与开发者深入交流,同步探索热门话题。',
},
{
url: 'https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/shouye/2025-07-10/600-kaifazhehuodong-0805.jpeg',
title: '开发者活动',
subTitle: '与专家深度交流,结识行业大咖,了解一手资讯。',
},
{
url: 'https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/shouye/2025-07-10/600-kaifazheyuekan-0805.jpeg',
title: '开发者月刊',
subTitle: '开发者联盟 App 鸿蒙版全新上线,邀你尝鲜。',
},
{
url: 'https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/kaifarumen-600x440-0718.jpg',
title: '开发入门,欢迎启程',
subTitle: '实现所有想法,只需迈出一步。',
}
]
const activityList: ExploreItemData[] = [
{
url: 'https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20250512142719.02205984243008185029215264961028:50001231000000:2800:B2F5B5089F5C962952AA806C7E90B30E0394C6ECEB13CCC0110B451A0F71BCFD.jpg',
title: 'HarmonyOS组件/模板集成创新活动',
subTitle: '探索HarmonyOS组件/模板,打造属于您的鸿蒙应用。这是一场“码”出效率的battle,挥洒创意,赢取精美大礼!',
},
{
url: 'https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20250312104827.49942723156750196759593747629725:50001231000000:2800:C27CF6B2E178E987FA5BD05235F87D583A17B1A6F6B2DDB3DA213350BFBBEDD2.jpg',
title: 'HarmonyOS百校未来星活动招募',
subTitle: '高校老师可通过参与本次活动开设HarmonyOS线上班级,获得官方课程与认证考试资源。',
},
{
url: 'https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20250627141450.84981751240408587404945346634763:50001231000000:2800:78EF60F05846773C538C11988C94BB6C2DC464F59541A43ED52DB41550C34A6E.png',
title: 'HarmonyOS培训伙伴启航行动',
subTitle: 'HarmonyOS赋能活动致力于培养具备HarmonyOS专业技能的人才,帮助开发者掌握HarmonyOS的开发、设计、管理和维护等能力,推动HarmonyOS技术在各行业的广泛应用。',
},
{
url: 'https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20250126154522.63830645029785252013343142880128:50001231000000:2800:C9101A886EEAE012D2435F669A28F68405C50E20EA0A0C1A82D08BEFF01887AA.jpg',
title: '华为应用市场新投应用权益活动',
subTitle: '针对新推广应用的“出圈”获量需求,华为应用市场推出了新投应用权益活动。新投应用的开发者们可以通过以下活动报名冲刺达标,获得相应的增值权益资源。',
},
{
url: 'https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20241225135516.45021618004205239446161062388780:50001231000000:2800:CAB0816764B103C0CAE5F0CAC8809B063CE340A664867FB65F1589D63B151473.jpg',
title: '华为支付激励计划--鸿蒙生态(实物商品/服务)',
subTitle: '本激励计划的时间为2024年12月15日至2025年12月30日,在此期间您在鸿蒙原生应用、元服务完成华为支付开发上线并报名参加此激励计划,我们将基于您在鸿蒙原生应用、元服务上华为支付产生的有效交易订单为您发放激励!',
},
{
url: 'https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20250117095530.81410890079695281441970669816287:50001231000000:2800:C39CA4C34ADC4079FE5DAA62ED8FBC30C78F6C8680D5181DCDD8FB1FFB89D6D1.png',
title: '耀星计划',
subTitle: '全场景数字服务创新,10亿资源倾力打造智慧数字云服务创新生态!',
},
{
url: 'https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20250121162513.48404348900314244740138254594788:50001231000000:2800:D2A05A2476C591740251BE598A4EC2ADB49375617F248170922AB67B5F54E135.png',
title: 'DESIGN FOR HUAWEI生态资源池建设--生态合作伙伴公开招募',
subTitle: 'DESIGN FOR HUAWEI(简称DFH)是华为智能手机、平板/笔记本电脑、可穿戴设备等配件合作伙伴计划。',
},
{
url: 'https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20250123144913.84036558888628087219848541816496:50001231000000:2800:8AB4B8AAFE6481DBF79022982F511777AFC351F405BFA30216546533D19979FE.jpg',
title: 'HUAWEI DEVELOPER DAY',
subTitle: 'HUAWEI DEVELOPER DAY(HDD)华为开发者日是华为终端与开发者深度交流的平台,通过主题演讲、闭门研讨、动手实验室和展区交流等活动将HarmonyOS的新技术及华为终端的全新开放能力及服务赋能给开发者。',
}
]
export const getSwiperListAPI = async ()=>{
return new Promise<SwiperItemData[]>((resolve)=>{
resolve(swiperList)
})
}
export const getStudyListAPI = async ()=>{
return new Promise<ExploreItemData[]>((resolve)=>{
resolve(studyList)
})
}
export const getActivityListAPI = async ()=>{
return new Promise<ExploreItemData[]>((resolve)=>{
resolve([...activityList.sort(()=>Math.random()-0.5)])
})
}