页面慢到想砸手机?鸿蒙应用性能优化不该只靠“祈祷”
大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
说句实在话,大多数性能问题,都不是“技术不行”,而是“我知道有问题,但我懒得拆”。
启动慢?——“设备配置一般就这样”;
列表卡?——“数据太多没办法”;
内存越来越高?——“这应用功能多嘛”;
网络慢?——“后端锅,跟我没关系”。
直到有一天,测试丢过来一句:
“你这个 App 打开要 3 秒,我刚刚点完都怀疑没点上。”
你才会认真坐下来:好,我们聊聊性能。
今天这篇,就是一份面向鸿蒙应用的 性能优化“全身检查”指南,围绕你给的五个方向,一条一条拆开讲:
- 启动速度优化
- UI 卡顿定位与优化
- 内存分析
- 布局过度渲染优化
- 网络优化建议
不讲空话,尽量都落到“你可以马上改点什么”的程度。
一、启动速度优化:用户点图标之后发生了什么?
一句大实话:
启动时间不是拿来“堆逻辑”的,而是拿来“做减法”的。
我们先把启动阶段拆一下,你才能知道该砍哪儿。
1.1 启动流程拆解(以 Stage 应用为例)
典型流程大概是:
- 进程创建 → runtime 初始化
UIAbility.onCreate→onWindowStageCreatewindowStage.loadContent('pages/XXX')- 页面
@Entry组件aboutToAppear/build - 首屏数据加载(网络/本地)
- 首帧绘制完成(用户“感觉”到应用打开了)
真正影响“体感”的,是 第 1 次可交互 的时间:
- 不是“所有数据都回来”
- 而是:“我能看到东西,并且能点东西”
所以启动优化本质就两件事:
- 砍掉首屏不必要的工作(延后 / 懒加载)
- 让首屏尽快可见(占位 / 骨架屏 / 部分数据)
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 还没画完你已经在疯狂等网络了。
更合理的做法:
- 能本地缓存的尽量走本地(如上次的配置、Tab 名称等);
- 拆优先级:首屏必须数据 → 次要数据;
- 部分区域用骨架屏占位。
示例:
@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 卡顿优化就两件事:
- 找到哪儿超时了(谁在主线程上瞎忙)
- 要么拆,要么挪(异步 / 预计算 / 降复杂度)
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 等;
- 把复杂逻辑放在手势结束 / 间隔回调里;
- 自绘组件里尽量只用轻量计算运算。
三、内存分析:不泄漏,其实已经赢一半
内存问题一般有两类:
- 泄漏:东西本该释放却没释放
- 高峰太高:某些场景瞬间占用过大(大图、批量数据)
这两类都值得你“专门过一遍”。
3.1 常见泄漏场景(你很可能已经踩过)
- 全局单例持有 UI 上下文 / 组件实例
class GlobalBus {
static ctx: UIAbility | null = null; // ❌
}
UI 结束了,但这个引用还在,就释放不了。
任何“长生命周期对象(单例 / 静态)” 上,尽量不要挂 UI 对象。
- 没取消的定时器 / 监听
aboutToAppear() {
this.timer = setInterval(() => {
this.loadData();
}, 1000);
}
aboutToDisappear() {
// ❌ 忘了 clearInterval
}
组件销毁后,定时器还在引用它 → 内存无法回收。
习惯:凡是 on / setInterval / subscribe,就一定有 off / clear / unsubscribe。
- 无限增长的列表 / 缓存
一些“缓存”写着写着就变成“内存黑洞”:
class ImageCache {
static map: Map<string, PixelMap> = new Map(); // ❌ 无上限
}
建议:
- 设置最大大小(LRU 思路)
- 某些场景清理缓存(退出页面 / 低内存提示)
3.2 内存分析的基本套路
哪怕不打开任何高级工具,你也可以先用这几个习惯:
-
场景化检测
- 连续开关一个大页面(比如带地图 / 相机 / Web 的页面)
- 看内存是否持续上升,不回落
-
关注“长生命周期对象”
- 单例、全局 store、全局事件总线
- 尽量不让它们直接持有 UI 实例 / 大对象
-
软引用 / 持久化替代
- 大数据、历史记录用磁盘存储而不是一直挂内存
目标不是“压到极低”,而是“没有不受控的增长曲线”。
四、布局过度渲染:不是所有的 Column+Row 套娃都无害
很多卡顿、内存、绘制压力,其实都“长”在布局上。过深的树 + 过频的重建,再好的设备也脾气不好。
4.1 深度嵌套是性能的天敌
见过类似结构吗:
Column() {
Row() {
Column() {
Row() {
Stack() {
Column() {
// ...
}
}
}
}
}
}
“能画出来” ≠ “写得对”。
建议:
- 用
Flex或Grid简化复杂组合,而不是无脑 Row/Column 拼起来; - 能合并的容器尽量合并:有些外层 Column 完全多余;
- 大量重用的区域单独做成组件,避免在一次
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 会不会在列表里很吃力?”
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐


所有评论(0)