鸿蒙|性能优化-渲染丢帧优化

22 阅读7分钟

概览:本文深入解析渲染丢帧的成因,通过渲染范围控制、布局节点精简、组件绘制优化、状态刷新控制、动画帧率提升、视觉流畅感知六大方向,系统介绍保障界面流畅的关键技术。并通过“高负载场景的分帧渲染(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        widthheight、Size
transform    /
widthheight等布局属性的改变,是需要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)])
  })
}