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

本文目录:

前言

老实讲,我第一次把一个鸿蒙应用装到真机上,点下图标的那一刻,心里其实挺紧张的:
——到底是“秒开”,还是“转圈圈现场翻车”?

结果当然是:转。圈。圈。
那一瞬间我意识到一个残酷的事实:功能跑起来 ≠ 应用体验好,而从“能用”到“好用”,最硬的一道坎,就是——性能

这篇文章,我就想和你把这个坎彻底抹平。我们从五个维度,把鸿蒙应用的性能问题拆开来、掰碎了聊:

  • 启动速度优化
  • UI 卡顿定位与优化
  • 内存分析
  • 布局过度渲染优化
  • 网络优化建议

不整虚的,有原则、有方法、有代码示例、有实战思路,写完这些,你再看自己 App 的时候,心里就不是“千万别卡”,而是——“来吧,随便测” 😏

🧭 一、先说重点:性能优化的正确姿势是什么?

很多人一提性能优化,第一反应是:
“是不是要在所有地方都用缓存、用懒加载、把东西都放到后台线程?”

不,真的别瞎搞。

性能优化最重要的是两句话:

  1. 先测,再动手:不要拍脑袋优化。没监控、没数据、只靠‘感觉’,多半会把时间浪费在错的地方。
  2. 抓主线,不纠结细节:用户最在意什么?启动速度、是否卡顿、耗不耗电、网是不是很慢。先把“大痛点”干掉。

所以,接下来我们就按真实使用路径来拆:
启动 → 首屏渲染 → 交互流畅度 → 内存 → 布局 → 网络。


🚀 二、启动速度优化:从“点一下等半天”到“点一下就开”

2.1 冷启动、热启动、温启动,先搞清楚你在优化哪个

  • 冷启动:首次启动 / 进程被杀后再次启动,系统需要重新创建进程、初始化运行时、加载 Ability,这一段最费时间。
  • 热启动:App 还在前台或刚退到后台,回到前台几乎秒开。
  • 温启动:进程存在,但部分组件需要重建,速度介于两者之间。

我们最常优化的是:冷启动首屏时间

2.2 启动路径梳理:别没搞清流程就开始瞎改

以 Stage 模型为例,一个典型启动流程:

  1. 系统创建进程
  2. 加载 ArkTS 运行时
  3. 加载应用 Entry Ability
  4. onCreate() / onWindowStageCreate() 执行
  5. 首屏 UI build 渲染
  6. 首帧呈现

你要做的,是:

  • 把非必要的初始化移到后台 / 延迟
  • 把和首屏无关的逻辑统统“靠后排队”

2.3 反例:把所有初始化全塞在 onWindowStageCreate 里

// ❌ 典型反面示例:全塞启动期
onWindowStageCreate(windowStage: window.WindowStage) {
  // 初始化日志系统
  LogManager.init();

  // 初始化网络 SDK
  NetworkClient.initHeavy();

  // 预加载配置、拉取远端配置
  ConfigManager.loadRemoteConfigSync();

  // 初始化数据库、预加载数据
  UserRepository.initDbAndLoadAll();

  // 再开始加载 UI
  windowStage.loadContent('pages/Index');
}

这样写的结果就是:
图标点下去 → 白屏 → 白屏 → 继续白屏 → 用户删你 App 😇

2.4 正解:首屏优先,其他一律延迟 / 异步

@Entry
@Component
struct Index {
  @State isReady: boolean = false;

  aboutToAppear() {
    // 1. UI 先起来
    this.isReady = false;

    // 2. 后台异步初始化
    // 注意不要阻塞 UI 线程
    globalThis.setTimeout(async () => {
      await initAppCore(); // 网络、日志、数据库等
      this.isReady = true;
    }, 0);
  }

  build() {
    Column() {
      if (!this.isReady) {
        // 轻量的启动占位 UI
        Text('启动中…')
          .fontSize(20)
        LoadingProgress()
      } else {
        HomePage(); // 真正业务首页
      }
    }
  }
}

🔑 核心思路:

  • UI 立即渲染 → 用户有反馈
  • 复杂初始化走后台,完成后再切业务页
  • 启动阶段别做任何“看不见的重活”

2.5 启动优化常见手段清单 ✅

  • ✅ 在 onWindowStageCreate 中仅做:

    • 必需:路由初始化、首屏加载
    • 不要:大量业务逻辑、日志、埋点全同步初始化
  • ✅ 使用懒加载(例如:次级页面模块按需 import)

  • ✅ 启动图片、占位骨架屏,让用户“看见东西”

  • ✅ 资源瘦身:

    • 删除无用图片、音频
    • 在打包时启用压缩
  • ✅ 异步初始化 SDK(埋点、IM、推送等)

一句话总结:启动阶段只干“非做不可的事”,其他的都“排队等会儿再说”。


🎨 三、UI 卡顿定位与优化:不是手机不行,是你太“上头”

说到卡顿,用户其实不会说“你掉帧了”,他们只会说:

“你这个 App 怎么用着用着就不动了?”

从技术角度讲:

  • UI 渲染是以帧为单位的(常见 60fps)。
  • 一帧耗时 > 16.6ms,就有卡顿的可能。

3.1 卡顿本质:UI 线程被干扰了

鸿蒙 UI 构建 + 渲染主要跑在主线程。当你在主线程里干下面这些事时,就会卡:

  • 大量循环计算
  • 复杂 JSON 解析
  • IO 操作(文件/数据库/网络)
  • 动画里不停 setState

3.2 典型错误示例:在 UI 构建周期里做重计算

@Entry
@Component
struct HeavyPage {
  @State list: number[] = [];

  aboutToAppear() {
    // ❌ 在生命周期直接跑重计算
    this.list = this.heavyCalc();
  }

  heavyCalc(): number[] {
    let arr: number[] = [];
    for (let i = 0; i < 500000; i++) {
      arr.push(i * i);
    }
    return arr;
  }

  build() {
    Column() {
      List() {
        ForEach(this.list, item => {
          ListItem() {
            Text(item.toString());
          }
        })
      }
    }
  }
}

这样干有多可怕?

  • 页面切换过去 → 界面一顿一顿
  • 系统觉得你太“阻塞”,可能直接 ANR 风险

3.3 正确姿势:重任务移到后台 / 分片处理

async function heavyCalcChunked(): Promise<number[]> {
  let arr: number[] = [];
  const CHUNK = 5000;

  return new Promise(resolve => {
    let i = 0;
    function runChunk() {
      const start = Date.now();
      for (; i < 500000; i++) {
        arr.push(i * i);
        if (i % CHUNK === 0 && (Date.now() - start) > 8) {
          // 切分为多帧执行,避免长时间阻塞
          globalThis.setTimeout(runChunk, 0);
          return;
        }
      }
      resolve(arr);
    }
    runChunk();
  });
}

@Entry
@Component
struct SmoothPage {
  @State list: number[] = [];
  @State loading: boolean = true;

  aboutToAppear() {
    heavyCalcChunked().then(result => {
      this.list = result;
      this.loading = false;
    });
  }

  build() {
    Column() {
      if (this.loading) {
        Text('计算中,别急…');
      } else {
        List() {
          LazyForEach(this.list, (item: number) => {
            ListItem() {
              Text(item.toString());
            }
          })
        }
      }
    }
  }
}

🔑 思路:

  • 大任务切成小块,每一帧只做一点点
  • setTimeout、后台任务机制拆分
  • UI 始终保持可响应

3.4 列表滑动卡顿优化

高频痛点:List / LazyForEach 滑动时一卡一卡。

常见原因:

  • ListItem 内容过于复杂,布局层级深
  • 每次滑动都会触发复杂计算
  • 图片加载没做缓存、没做占位图
  • @State 状态粒度过大,导致整树重建

优化建议:

  1. 使用 LazyForEach 而不是 ForEach
LazyForEach(this.items, (item) => {
  ListItem() {
    UserRow({ user: item });
  }
})
  1. 子项组件拆分,避免父组件重建整棵树:
@Component
struct UserRow {
  @Prop user: User;

  build() {
    Row() {
      // ...
    }
  }
}
  1. 图片加载优化:

    • 小图用本地资源
    • 网络图片合理设置大小、缓存
  2. 避免在 build() 中做任何复杂逻辑。
    build() 只负责描述 UI,不要写逻辑。


🧠 四、内存分析:泄漏不是“可能会发生”,是“肯定在发生”

性能问题里有一种最阴险的,就是内存泄漏:
用的时候感觉还好,跑久了开始慢、卡、最后直接崩。

鸿蒙应用中常见的泄漏来源:

  • 全局变量 / 单例里强引用 Context / UI 对象
  • 事件订阅未取消
  • 定时器(setInterval / setTimeout)没清理
  • 长生命周期对象持有大量短生命周期数据

4.1 经典泄漏示例一:全局缓存 UI 对象

// ❌ 完全错误示范
class GlobalCache {
  static instance = new GlobalCache();
  page?: SomePage; // 持有 UI 结构体实例 ❌
}

@Entry
@Component
struct SomePage {
  aboutToAppear() {
    GlobalCache.instance.page = this; // 泄漏大户
  }
}

为什么有问题?

  • struct 组件的生命周期由框架管理,你把 this 丢到全局,GC 就没法正确回收。
  • 页面关闭后,本来应该释放的组件树,被你的全局引用强行“续命”。

4.2 正确姿势:缓存数据,而不是缓存 UI 实例

class GlobalData {
  static instance = new GlobalData();
  lastUserName: string = '';
}

@Entry
@Component
struct SomePage {
  @State name: string = '';

  aboutToDisappear() {
    GlobalData.instance.lastUserName = this.name;
  }
}

🌟 记住原则:全局只可以缓存“纯数据”,不要缓存 UI / Context / Window 等重对象。

4.3 经典泄漏示例二:注册监听不取消

class Bus {
  private listeners: Array<() => void> = [];
  add(listener: () => void) {
    this.listeners.push(listener);
  }
  // ...
}

const bus = new Bus();

@Entry
@Component
struct ListenPage {
  aboutToAppear() {
    bus.add(() => {
      // 使用 this / 状态
    });
  }
}

如果监听回调里引用了 this 或状态,就相当于间接把整个组件挂在了 bus 上。页面关掉了,监听还在。

✅ 正确写法:

  • 支持解绑,组件销毁时手动移除
  • 使用弱引用机制(如果底层支持)
class Bus {
  private listeners: Map<number, () => void> = new Map();
  private idGen: number = 0;
  add(listener: () => void): number {
    const id = this.idGen++;
    this.listeners.set(id, listener);
    return id;
  }
  remove(id: number) {
    this.listeners.delete(id);
  }
}

const bus = new Bus();

@Entry
@Component
struct ListenPage {
  private listenId: number = -1;

  aboutToAppear() {
    this.listenId = bus.add(() => {
      // handle…
    });
  }

  aboutToDisappear() {
    if (this.listenId >= 0) {
      bus.remove(this.listenId);
    }
  }
}

4.4 内存分析步骤(实战套路)

  1. 先观察现象

    • 使用一段时间后明显变慢?
    • 多次打开/关闭同一页面后崩溃?
  2. 工具分析(如果你接触到 Profiler 等工具)

    • 看内存曲线是否呈阶梯型,只升不降。
    • 采集 Heap Snapshot,查看大对象引用链。
  3. 代码排查重点

    • 全局单例
    • 事件 Bus
    • 定时器
    • 静态变量

一般来说,你 80% 的内存问题,都能在“全局 + 监听 + 定时器”这三个地方找到源头。


🧱 五、布局过度渲染优化:不是你机子差,是你布局太“作”

有时,你明明没做什么重计算,结果界面就是不流畅。那很可能是——布局本身在拖后腿。

5.1 什么叫“过度渲染”?

  • 无意义的重复 UI 构建
  • 无关状态更新却导致整个 UI 树重建
  • 布局嵌套层级非常深
  • 大量组件在视图外也在持续参与布局/绘制

5.2 一个真实的反例:状态挂太高,牵一发动全身

@Entry
@Component
struct BigPage {
  @State selectedId: number = -1;

  build() {
    Column() {
      // 顶部导航
      Header();

      // 中间列表,项目非常多
      List() {
        LazyForEach(this.getItems(), (item) => {
          ListItem() {
            ItemRow({
              item: item,
              selected: this.selectedId === item.id,
              onClick: (id: number) => this.selectedId = id
            });
          }
        });
      }

      // 底部信息面板,依赖 selectedId
      FooterInfo({ id: this.selectedId });
    }
  }
}

看起来很合理对吧?
问题在于:

  • 每次 selectedId 改变,BigPage.build() 整体重跑。
  • 整个 LazyForEach 会重新构建(尽管有一定复用,仍然有开销)。

5.3 优化手段一:拆分组件,缩小重建范围

@Entry
@Component
struct BigPage {
  @State selectedId: number = -1;

  build() {
    Column() {
      Header()
      ItemList({
        selectedId: this.selectedId,
        onSelect: (id: number) => this.selectedId = id
      })
      FooterInfo({ id: this.selectedId })
    }
  }
}

@Component
struct ItemList {
  @Prop selectedId: number;
  @Prop onSelect: (id: number) => void;

  build() {
    List() {
      LazyForEach(getItems(), (item) => {
        ListItem() {
          ItemRow({
            item: item,
            selected: this.selectedId === item.id,
            onClick: this.onSelect
          })
        }
      })
    }
  }
}

虽然还是会重建 ItemList,但拆分之后:

  • 顶部 Header 与底部 Footer 不因列表内部逻辑而重建
  • 进一步可以在 ItemRow 内做优化(例如避免过度绑定状态)

5.4 优化手段二:局部状态下沉

如果可以,甚至可以用 @Link 把选中状态下沉到列表层级,而不是挂在顶层。

原则:状态颗粒度越精细、越靠近真正使用它的组件,重建范围就越小。

5.5 布局层级过深的问题

比如:

Column() {
  Row() {
    Column() {
      Row() {
        Column() {
          // 再套一堆…
        }
      }
    }
  }
}

层级太深会导致:

  • 布局计算时间变长
  • 每次重绘成本变高

优化方案:

  • 适当使用 Flex 简化布局
  • 复用 UI 模块(组件化拆分)
  • 避免无意义的包一层又一层

🌐 六、网络优化建议:你的 App 慢,不一定是网慢,是你“用网方式不对”

很多应用一到真实网络环境下就露馅:
Wi-Fi 还好,一到 4G / 信号不稳的小区电梯间,直接卡死。

你当然可以怪用户“为啥不用 5G”,但更实际的做法是:认认真真把网络逻辑优化好。

6.1 基本原则:

  1. 请求要少:能合并就合并
  2. 数据要小:能减就减
  3. 缓存要用:能复用就复用
  4. 超时要合理:别一直耗着

6.2 请求优化常见手段

① 减少请求次数
  • 多个接口合并为一个批量接口
  • 滑动加载列表时做节流(防止用户来回滑动就狂发请求)
// ❌ 每次滑到底都立即请求
// ✅ 用定时器+标志位做简单节流
② 使用合适的超时与重试策略
class HttpClient {
  async get(url: string, options?: HttpOptions): Promise<Response> {
    // 设置合理超时,例如 5~8 秒,而不是无限等
  }
}
  • 不要无限重试
  • 不要在 UI 线程等待同步网络
③ 启用缓存(本地 + 内存)

典型模式:

  • 先读本地缓存 → 立即展示
  • 再发网络请求 → 若有新数据再更新 UI
@State list: Item[] = [];
@State loading: boolean = true;

aboutToAppear() {
  // 1. 本地缓存优先
  this.list = LocalCache.load() ?? [];
  this.loading = this.list.length === 0;

  // 2. 后台拉新数据
  fetchFromNet().then(newList => {
    this.list = newList;
    this.loading = false;
    LocalCache.save(newList);
  }).catch(_ => {
    this.loading = false;
  });
}

用户体感:

“一打开就有内容,网好时自动变更新版本”

④ 网络失败的降级策略
  • 提示清晰:

    • “网络异常,请稍后再试”
    • “正在使用本地缓存数据”
  • 不要全屏弹窗挡住一切

  • 对次要功能失败“静默降级”

6.3 大流量优化

如果你的业务有:

  • 图片墙
  • 视频流
  • 大文件下载

建议:

  • 缩略图 / 预览图与原图分离
  • 图片懒加载(进入可视范围才请求)
  • 分片下载 + 断点续传(看业务需要)

一句话:别把用户的流量当不要钱,流量一疼,卸载速度会比你想的快多了。


🧾 七、综合实战:从“问题 App”到“丝滑 App”的优化路线图

如果你现在有一个运行还算正常、但体验不够好的鸿蒙 App,可以按下面的顺序一点点优化:

Step 1:启动优化

  • 统计启动时长(冷启动 / 首帧时间)
  • 把所有和首屏无关的初始化移到后台 / 延迟
  • 加上占位 UI / 启动页 / 骨架屏

Step 2:UI 流畅度

  • 找出滑动卡顿的页面
  • 检查是否在生命周期里做重计算
  • 优化列表:LazyForEach + 子项组件拆分

Step 3:内存与稳定性

  • 重点排查全局单例、监听、定时器
  • 多次打开关闭某一页面,观察内存曲线
  • 优化引用关系、解绑监听

Step 4:布局重构

  • 梳理复杂页面的布局树,压缩层级
  • 通过状态下沉减少不必要重建
  • 重用组件,减少重复 UI 结构

Step 5:网络体验

  • 引入本地缓存策略
  • 优化请求超时与重试
  • 使用渐进式加载 + 友好的错误提示

🎯 八、结语:性能优化不是“补丁”,是工程能力的象征

很多人对性能优化有误解:

“等上线了卡再说吧,先把功能做完。”

但现实是:

  • 启动慢,用户根本没耐心等你“功能有多强大”
  • UI 卡顿,用户根本不在乎你“逻辑有多复杂”
  • 内存泄漏,用户只看得到“怎么又崩了?”

而你如果能在做功能的过程中,时刻带着性能意识去写代码

  • 状态怎么设计更局部?
  • 数据该不该缓存?
  • 这个逻辑是不是会阻塞 UI?
  • 这个全局引用以后会不会回收不了?

那你就不仅仅是“会写代码的开发”,而是一个真的在做产品体验的人

鸿蒙应用性能优化,说难也难,说简单,其实就是一件事:
把用户当人,把设备当有限资源,把代码当工程,而不是当堆砌。

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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐