大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

说句实在话,大多数性能问题,都不是“技术不行”,而是“我知道有问题,但我懒得拆”。
启动慢?——“设备配置一般就这样”;
列表卡?——“数据太多没办法”;
内存越来越高?——“这应用功能多嘛”;
网络慢?——“后端锅,跟我没关系”。

直到有一天,测试丢过来一句:

“你这个 App 打开要 3 秒,我刚刚点完都怀疑没点上。”

你才会认真坐下来:好,我们聊聊性能。

今天这篇,就是一份面向鸿蒙应用的 性能优化“全身检查”指南,围绕你给的五个方向,一条一条拆开讲:

  1. 启动速度优化
  2. UI 卡顿定位与优化
  3. 内存分析
  4. 布局过度渲染优化
  5. 网络优化建议

不讲空话,尽量都落到“你可以马上改点什么”的程度。

一、启动速度优化:用户点图标之后发生了什么?

一句大实话:

启动时间不是拿来“堆逻辑”的,而是拿来“做减法”的。

我们先把启动阶段拆一下,你才能知道该砍哪儿。

1.1 启动流程拆解(以 Stage 应用为例)

典型流程大概是:

  1. 进程创建 → runtime 初始化
  2. UIAbility.onCreateonWindowStageCreate
  3. windowStage.loadContent('pages/XXX')
  4. 页面 @Entry 组件 aboutToAppear/build
  5. 首屏数据加载(网络/本地)
  6. 首帧绘制完成(用户“感觉”到应用打开了)

真正影响“体感”的,是 第 1 次可交互 的时间:

  • 不是“所有数据都回来”
  • 而是:“我能看到东西,并且能点东西”

所以启动优化本质就两件事:

  1. 砍掉首屏不必要的工作(延后 / 懒加载)
  2. 让首屏尽快可见(占位 / 骨架屏 / 部分数据)

1.2 能推迟的就不要放在启动路径上

看见这种代码,就要敲警钟了:

export default class MainAbility extends UIAbility {
  onCreate() {
    // ❌ 一大堆初始化
    Log.init();
    GlobalConfig.loadFromDisk();
    UserStore.restoreLoginState();
    Analytics.init();
    BigSdkA.init();
    BigSdkB.init();
    // ...
  }
}

建议改成:“冷热分层 + 懒初始化”

// app/AppInit.ets
export function initLightweight() {
  Log.init();
  Analytics.initLite();
}

export function initHeavyweightLater() {
  setTimeout(() => {
    BigSdkA.init();
    BigSdkB.init();
    GlobalConfig.loadFromDisk();
  }, 0); // 放到事件队列后面
}

onCreate / onWindowStageCreate 里只做 initLightweight(),真正重活放在 App 进入稳定状态后慢慢搞。

原则:
“不是为了首屏必须要做的事情,一律往后推。”


1.3 首屏数据:别一股脑全拉

典型错误:

aboutToAppear() {
  // ❌ 一进页面就拉一堆接口
  this.fetchUserInfo();
  this.fetchBanner();
  this.fetchTabConfig();
  this.fetchRecommendList();
  this.fetchNotice();
}

首屏就卡在这几个 Promise 上,UI 还没画完你已经在疯狂等网络了。

更合理的做法:

  1. 能本地缓存的尽量走本地(如上次的配置、Tab 名称等);
  2. 拆优先级:首屏必须数据 → 次要数据;
  3. 部分区域用骨架屏占位。

示例:

@Entry
@Component
struct HomePage {
  @State user: User | null = null;
  @State tabs: TabConfig[] = [];
  @State recList: Item[] = [];
  @State loadingRec: boolean = true;

  aboutToAppear() {
    // 1. 先用上次缓存 & 默认配置顶上
    this.tabs = LocalCache.getTabs() ?? defaultTabs;

    // 2. 立即请求首要数据
    UserApi.getProfile().then(u => this.user = u);

    // 3. 列表可以稍微“晚一点”再来
    RecommendApi.getList().then(list => {
      this.recList = list;
      this.loadingRec = false;
    });
  }

  build() {
    Column() {
      HomeHeader({ user: this.user })
      HomeTabs({ tabs: this.tabs })

      if (this.loadingRec) {
        RecListSkeleton()
      } else {
        RecList({ items: this.recList })
      }
    }
  }
}

首屏 UI 很快能出来,列表部分则由骨架变成真实列表,用户感知完全不同


1.4 几个能立刻检查的启动问题

可以抽时间查一下自己项目里有没有这些“启动杀手”:

  • onCreate / 页面 aboutToAppear 里做耗时 IO、同步大文件读写
  • ⛔ 一启动就初始化所有三方 SDK(埋点、推送、分享、广告……全部一起)
  • ⛔ 一进入首页就拉 5 个以上接口,而且都在 UI 渲染之前 await
  • ⛔ 首页组件一上来就做复杂计算(大 JSON 解析、图片 Base64 转换等)

能改掉这几条,你家 App 的“手感”一般都会好一截。


二、UI 卡顿:你以为是设备差,其实是你主线程太忙

60 FPS 意味着每帧 16.67ms 的时间预算。你在这 16ms 里做了太多计算、布局、绘制,它就掉帧了。

所以 UI 卡顿优化就两件事:

  1. 找到哪儿超时了(谁在主线程上瞎忙)
  2. 要么拆,要么挪(异步 / 预计算 / 降复杂度)

2.1 常见“卡顿源头”清单

实战里,经常看到这些“罪魁祸首”:

  • ❌ 大量 @State 在一个组件里变来变去,导致整块 UI 重建
  • build() 里做运算 / 过滤 / 排序 / map 一大堆
  • ❌ 列表滚动时同步做网络请求 / IO
  • ❌ 动画里每一帧做复杂计算(特别是自绘组件)
  • ❌ 使用超复杂嵌套布局(好几层 Column + Row + Stack 套娃)

2.2 拆组件,别让一个 build 扛全世界

ArkUI 是声明式的:状态变 → 对应的组件树重建
如果你把一整页都写在一个大组件里,那一点小状态变动都会导致整块刷新。

差别在这:

// ❌ 全写在一个组件里
@Entry
@Component
struct BigPage {
  @State tabIndex: number = 0;
  @State list: Item[] = [];
  @State banner: Banner[] = [];
  @State filter: Filter = {};

  build() {
    Column() {
      // Tab + Banner + List + Footer 全在一起
    }
  }
}

改成:

@Entry
@Component
struct BigPage {
  @State tabIndex: number = 0;

  build() {
    Column() {
      HomeHeader()
      HomeTabs({ index: this.tabIndex, onChange: i => this.tabIndex = i })
      HomeListSection({ tabIndex: this.tabIndex })
      FooterBar()
    }
  }
}

@Component
struct HomeListSection {
  @Prop tabIndex: number;
  @State list: Item[] = [];

  aboutToAppear() {
    this.loadData();
  }

  private loadData() {
    // 根据 tabIndex 拉列表
  }

  build() { /*...*/ }
}

这样:

  • Tab 切换主要影响 HomeListSection
  • Header / Footer 几乎不参与重建;
  • HomeListSection 又可以进一步拆 List 子组件。

一句话:
要么让状态“下沉”(更接近真实变化的地方),要么让视图“拆开”(减少受影响范围)。


2.3 避免在 build() 里做重逻辑

典型反面例子:

build() {
  const filtered = this.list
    .filter(...)
    .sort(...)
    .map(...);   // ❌ 每次 build 都重新算一遍

  Column() {
    ForEach(filtered, item => ...)
  }
}

假如这个组件因为任意一个 @State 改变而重建,这些计算就被白干无数遍。

优化方式:

  • 计算结果缓存到 @State / 私有字段,放到事件回调里触发
  • 或者用简单的“脏标记”做增量更新

示例:

@State private displayList: Item[] = [];

private recomputeDisplayList() {
  this.displayList = this.list
    .filter(...)
    .sort(...)
    .map(...);
}

build() {
  Column() {
    ForEach(this.displayList, item => ...)
  }
}

把计算从“每次 build 时做”改成“数据变化时做”,UI 就轻松很多。


2.4 动画卡顿:别在每一帧干重活

手势联动 + 动画 时,尤其容易踩坑:

PanGesture()
  .onActionUpdate((e) => {
    // ❌ 每一帧都在算复杂几何 / 调接口 / 写日志
    this.doHeavyCompute(e);
  })

正确思路:

  • 更新少量“驱动 UI 的状态”:offset, progress 等;
  • 把复杂逻辑放在手势结束 / 间隔回调里;
  • 自绘组件里尽量只用轻量计算运算。

三、内存分析:不泄漏,其实已经赢一半

内存问题一般有两类:

  1. 泄漏:东西本该释放却没释放
  2. 高峰太高:某些场景瞬间占用过大(大图、批量数据)

这两类都值得你“专门过一遍”。


3.1 常见泄漏场景(你很可能已经踩过)

  1. 全局单例持有 UI 上下文 / 组件实例
class GlobalBus {
  static ctx: UIAbility | null = null; // ❌
}

UI 结束了,但这个引用还在,就释放不了。

任何“长生命周期对象(单例 / 静态)” 上,尽量不要挂 UI 对象。


  1. 没取消的定时器 / 监听
aboutToAppear() {
  this.timer = setInterval(() => {
    this.loadData();
  }, 1000);
}

aboutToDisappear() {
  // ❌ 忘了 clearInterval
}

组件销毁后,定时器还在引用它 → 内存无法回收。

习惯:凡是 on / setInterval / subscribe,就一定有 off / clear / unsubscribe


  1. 无限增长的列表 / 缓存

一些“缓存”写着写着就变成“内存黑洞”:

class ImageCache {
  static map: Map<string, PixelMap> = new Map(); // ❌ 无上限
}

建议:

  • 设置最大大小(LRU 思路)
  • 某些场景清理缓存(退出页面 / 低内存提示)

3.2 内存分析的基本套路

哪怕不打开任何高级工具,你也可以先用这几个习惯:

  1. 场景化检测

    • 连续开关一个大页面(比如带地图 / 相机 / Web 的页面)
    • 看内存是否持续上升,不回落
  2. 关注“长生命周期对象”

    • 单例、全局 store、全局事件总线
    • 尽量不让它们直接持有 UI 实例 / 大对象
  3. 软引用 / 持久化替代

    • 大数据、历史记录用磁盘存储而不是一直挂内存

目标不是“压到极低”,而是“没有不受控的增长曲线”。


四、布局过度渲染:不是所有的 Column+Row 套娃都无害

很多卡顿、内存、绘制压力,其实都“长”在布局上。过深的树 + 过频的重建,再好的设备也脾气不好。

4.1 深度嵌套是性能的天敌

见过类似结构吗:

Column() {
  Row() {
    Column() {
      Row() {
        Stack() {
          Column() {
            // ...
          }
        }
      }
    }
  }
}

“能画出来” ≠ “写得对”。

建议:

  1. FlexGrid 简化复杂组合,而不是无脑 Row/Column 拼起来;
  2. 能合并的容器尽量合并:有些外层 Column 完全多余;
  3. 大量重用的区域单独做成组件,避免在一次 build() 中堆太多节点。

4.2 列表过度渲染:一次性画太多

大列表场景下,如果你这么写:

ForEach(this.bigList, item => {
  HeavyItemView({ item });
})

如果列表本身不是懒加载型的(比如没有使用惰性布局),那它的所有子项都可能一次性参与布局 & 绘制,在数据稍微一多的时候非常顶不住。

建议:

  • 使用支持“按需渲染”的列表组件(Lazy 相关组件);
  • 分页加载,避免一次喂给 UI 1000 条;
  • 列表项内部尽量保持轻量级布局。

4.3 不要让动画 + 布局反复互相“触发”

比如你在动画中不断调整会影响布局的属性(如宽高),在某些嵌套结构里容易造成反复测量 / 布局。

能用 translate/scale 的,尽量不用 width/height 撑;
位置移动优先考虑位移而不是强行改变布局结构。


五、网络优化:别再用接口“生拽”性能了

网络这块最常见的心态是:“后端又不是我写的,我能怎么办。”
但其实前端 / 客户端能做的事情,比你想象的多很多。

5.1 减少“首屏依赖”的请求数

回到前面说的:首屏只保留“必须”的请求

常规操作:

  • 把“用户进入后才可能用得到”的请求放到交互触发里再发;
  • 多个请求合并为一个批量接口(如果后端支持);
  • 能通过上次数据 + 增量更新的,就不要每次全量拉。

5.2 请求节流 / 防抖

例如搜索输入框:

TextInput({ text: this.keyword })
  .onChange(v => {
    this.keyword = v;
    this.debounceSearch();
  })

debounceSearch 自己做个 300ms 防抖:

private searchTimer: number | undefined;

private debounceSearch() {
  if (this.searchTimer) clearTimeout(this.searchTimer);
  this.searchTimer = setTimeout(() => {
    this.realSearch();
  }, 300);
}

避免用户输入每个字都打一枪 API。


5.3 合理的超时、重试和降级

网络不可能永远好,性能的另一个侧面是:失败时也要“快”一点失败

  • 请求超时:不要放几十秒,移动网络下 5–8 秒已经足够判断重试或提示;
  • 重试策略:指数退避(2s → 4s → 8s),避免一窝蜂打爆后端;
  • 降级:重要模块失败 → 提示 & 给出“刷新 / 离线浏览”的选项,而不是卡死在 Loading。

5.4 静态资源和图片优化

尤其是首页一堆 Banner 的项目:

  • 控制图片尺寸,不要把 2000px 宽的大图缩成 300px 用;
  • 使用更高压缩率格式(如 WebP)而不是无脑 jpg/png;
  • 对于头像、icon 这类频繁使用资源,本地打包 + 缓存优先。

对用户来说,速度 = 体感;
对你来说,速度 = 请求数 × 体积 × 网络质量;
能动的两个变量(请求数 & 体积)都在你手上。


一个“可以马上干”的检查清单 ✅

如果你现在就想对自己的鸿蒙项目做一遍性能小体检,可以按这个列表过一遍:

启动相关

  • UIAbility.onCreate/onWindowStageCreate 是否有耗时 IO / 大量初始化?
  • 首屏是否只依赖 1–2 个关键接口?其它接口能否延后?
  • 是否有骨架屏 / 占位 UI,而不是白屏等待?

UI & 动画

  • 页面是否过于“大组件”,能否拆分成多个独立子组件?
  • build() 里有没有复杂计算(排序、过滤、map)?
  • 手势 / 动画回调里,有没有重逻辑(请求、写日志、大计算)?

内存 & 泄漏

  • 单例 / 全局对象是否持有 UI 上下文 / 组件实例?
  • 定时器 / 事件监听 / 订阅有没有对应的清理?
  • 列表缓存 / 图片缓存有没有上限?

布局 & 渲染

  • 是否存在过深的 Column/Row/Stack 套娃?
  • 列表是否支持懒渲染(Lazy List / 分页)?
  • 动画是否尽量用 transform 类属性(translate/scale/opacity),少改布局属性(width/height)?

网络

  • 首屏是否合并了可以合并的请求?
  • 搜索 / 输入等高频触发是否做了防抖/节流?
  • 超时 / 重试 / 降级策略是否明确?

性能优化这件事,说白了是对“时间”和“资源”的尊重:
对用户的时间、对设备的资源、对你未来自己维护这套代码的耐心。

它从来不是“某一天突然干一大波”的专项,而应该是你写每一个新页面、每一个新功能时顺带脑子里过一下的那句:

“这东西会不会拖慢启动?
这段逻辑要不要挪到后台?
这块 UI 会不会在列表里很吃力?”

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐