去年接手了一款已上线的工具类应用,运行在 HarmonyOS 5.0 上。用户反馈在老款设备上启动慢、列表滑动掉帧、后台容易被杀。作为团队唯一的鸿蒙开发,这个任务自然落到了我头上。起初以为只是简单调调参数就好,没想到深挖下去发现 HarmonyOS 5.0 的性能优化有不少门道。这半个月的调优过程让我收获不小,整理出来供大家参考。

一、冷启动优化:从 3.2 秒到 1.1 秒的实战

最先盯上的是冷启动速度。用 DevEco Studio 自带的性能分析器(Profiler)抓了一次 Trace,发现问题主要集中在两个地方:首屏渲染阻塞和非必要的资源加载。

1.1 首屏渲染并行化

我们的首页组件初始化时同步加载了三个数据源(天气、预报、空气质量),全部在 UI 线程执行,导致主线程被占用了近 800ms。HarmonyOS 5.0 新增的 TaskPool 并行任务能力正好可以用上。我把数据请求改成了并行异步加载:

import { taskpool } from "@kit.ArkTS";
import { http } from "@kit.NetworkKit";

@Concurrent
async function fetchWeatherData(cityCode: string): Promise<WeatherInfo> {
  const httpRequest = http.createHttp();
  const response = await httpRequest.request(
    `https://api.weather.com/${cityCode}`,
    { method: http.RequestMethod.GET }
  );
  return response.result as WeatherInfo;
}

@Concurrent
async function fetchForecast(cityCode: string): Promise<ForecastItem[]> {
  const httpRequest = http.createHttp();
  const response = await httpRequest.request(
    `https://api.weather.com/${cityCode}/forecast`,
    { method: http.RequestMethod.GET }
  );
  return response.result as ForecastItem[];
}

@Entry
@Component
struct WeatherHome {
  @State currentWeather: WeatherInfo = new WeatherInfo();
  @State forecastList: ForecastItem[] = [];

  async aboutToAppear() {
    const taskGroup = new taskpool.TaskGroup();
    taskGroup.addTask(fetchWeatherData("beijing"));
    taskGroup.addTask(fetchForecast("beijing"));
    taskGroup.addTask(fetchAirQuality("beijing"));

    const results = await taskpool.execute();
    this.currentWeather = results[0] as WeatherInfo;
    this.forecastList = results[1] as ForecastItem[];
  }
}

几个关键点:

  • @Concurrent 装饰器标记可并行执行的函数
  • 通过 TaskGroup 将多个任务打包,一次执行获取全部结果
  • 任务函数必须是纯函数,避免闭包引用外部状态

1.2 资源加载瘦身

原来的启动 Logo 用了一张 2.3MB 的 PNG 高清图。换为 SVG Vector 格式后体积直接降到 120KB,而且渲染速度更快。另外把字体文件从启动时同步加载改成了异步预加载,又省了 200ms:

// 在 EntryAbility 的 onCreate 中预加载字体
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  font.registerFont({
    familyName: "CustomFont",
    url: $r("app.media.custom_font"),
    fallback: "HarmonyOS Sans"
  }).then(() => {
    console.info("Font registered successfully");
  });
}

1.3 启动优先级策略

HarmonyOS 5.0 还提供了 UIAbilitygetLaunchExtra 接口,可以配置启动模式。对于我们的工具类应用,使用 LaunchType.SINGLETON 模式配合 specifiedWakeUp 配置,让系统对频繁唤醒做了优化:

@Entry
@Component
struct EntryAbility {
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 避免重复初始化
  }
}

冷启动从 3.2s 降到 1.1s,用户反馈瞬间就上来了。

二、列表滑动掉帧:从 45fps 到稳定 60fps 的调优

冷启动搞定后,又盯上了首页的城市列表滑动。60 个城市的数据,在中低端设备(麒麟 980 级别)上滑动只能跑到 45fps,偶尔还会卡顿一下。

2.1 定位问题:Profiler 分析

性能分析器显示主要是列表渲染时的 Layout 耗时过长,单次 layout 超过 16ms 的阈值。排查后发现每个列表项里嵌套了 4 层容器,用了 Circle 组件做头像背景,还叠加了阴影效果,渲染压力不小。

2.2 List 组件的正确打开方式

HarmonyOS 5.0 的 List 组件本身就做了懒加载,但前提是要正确配置。我做了两个关键优化:

开启缓存预渲染:

List() {
  ForEach(this.cityList, (item: CityItem) => {
    ListItem() {
      CityCard(city: item)
    }
  }, (item: CityItem) => item.cityId)
}
.cachedCount(5)
.edgeEffect(EdgeEffect.Spring)
.scrollBar(BarState.Auto)

简化列表项结构:

原来的 CityCard 嵌套了 Row > Column > Stack > Circle 四层容器。简化为单层 Row + borderRadius 属性:

@Component
struct CityCard {
  city: CityItem;

  build() {
    Row() {
      Image(this.city.avatarUrl)
        .width(48)
        .height(48)
        .borderRadius(24)
        .objectFit(ImageFit.Cover)

      Column() {
        Text(this.city.name)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
        Text(this.city.temperature)
          .fontSize(14)
          .fontColor("#666666")
      }
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 12 })
      .layoutWeight(1)

      Text(this.city.weatherIcon)
        .fontSize(24)
    }
    .width("100%")
    .padding(16)
    .backgroundColor(Color.White)
  }
}

2.3 其他优化细节

  • 列表项背景色:纯色背景直接用 backgroundColor 属性,不要创建额外的 Color 组件,少一层组件就少一次 Layout
  • 图片预加载:配合 cachedCount 提前加载下一屏图片数据
  • 避免 setState 风暴:滑动过程中不要在回调里频繁修改状态,使用 onScrollIndex 代替 onScroll 监听

优化后列表滑动稳定在 60fps,连最低端的测试设备都没再掉帧。

三、内存占用:从 280MB 降到 165MB 的细节

性能调优不能只看速度,内存也很关键。我们应用的内存一度达到 280MB,在部分设备上会被系统主动杀进程。

3.1 内存分析工具

用 DevEco Studio 的 Memory Profiler Dump 了几次快照(Heap Snapshot),发现两个主要问题:图片加载未及时释放和定时器泄漏。

3.2 图片缓存策略

原来使用 Image 组件加载网络图片,每次切换页面旧图片不会被回收。换成 HarmonyOS 5.0 推荐的 AsyncImage,配合缓存策略:

AsyncImage({ url: this.imageUrl })
  .width("100%")
  .height(200)
  .cachePolicy(CachePolicy.CacheFirst)
  .resizable(true)
  .placeholder($r("app.media.placeholder"))
  .error($r("app.media.error"))

CacheFirst 策略优先使用内存缓存,只有缓存不存在时才从网络加载,有效减少重复请求和内存分配。

3.3 定时器生命周期管理

定时器泄漏是因为在 aboutToDisappear 里忘记清理了。我封装了一个 TimerManager,自动管理生命周期:

export class TimerManager {
  private timers: Set<number> = new Set();

  public setTimeout(callback: () => void, delay: number): number {
    const timer = setTimeout(() => {
      callback();
      this.timers.delete(timer);
    }, delay) as number;
    this.timers.add(timer);
    return timer;
  }

  public setInterval(callback: () => void, delay: number): number {
    const timer = setInterval(callback, delay) as number;
    this.timers.add(timer);
    return timer;
  }

  public clearAll(): void {
    this.timers.forEach(timer => clearTimeout(timer));
    this.timers.clear();
  }
}

在组件中使用时,配合 aboutToAppearaboutToDisappear 自动清理:

@Component
struct WeatherDetail {
  private timerManager: TimerManager = new TimerManager();

  aboutToAppear() {
    this.timerManager.setInterval(() => {
      this.refreshData();
    }, 60000);
  }

  aboutToDisappear() {
    this.timerManager.clearAll();
  }
}

3.4 其他内存优化

  • 避免大对象常驻内存:使用完的数组、对象及时置 null
  • 控制图片分辨率:根据显示尺寸动态缩放,不要加载原图
  • 使用 @ObjectLink 代替 @State:对于嵌套对象,@ObjectLink 只更新修改的属性,减少不必要的渲染

内存占用降到了 165MB,在同类型应用中算是比较优秀的水平。

四、网络与电量优化

内存之外,网络请求频率和电量消耗也是用户感知强烈的指标。

4.1 请求合并与缓存

将多个分散的 API 请求合并为一个批量接口,减少网络往返次数。同时利用 HTTP 缓存头(Cache-ControlETag)减少重复请求:

import { http } from "@kit.NetworkKit";

const httpRequest = http.createHttp();
httpRequest.request("https://api.weather.com/batch", {
  method: http.RequestMethod.POST,
  header: {
    "Content-Type": "application/json",
    "Cache-Control": "max-age=300"
  },
  extraData: {
    cities: ["beijing", "shanghai", "guangzhou"]
  }
});

4.2 后台任务管理

HarmonyOS 5.0 对后台任务有严格限制。使用 WorkScheduler 代替常驻后台的定时器,让系统智能调度任务执行时机:

import { workScheduler } from "@kit.WorkSchedulerKit";

const workInfo: workScheduler.WorkInfo = {
  workId: 1001,
  bundleName: "com.example.weather",
  abilityName: "WeatherService",
  parameters: { cityCode: "beijing" },
  networkType: workScheduler.NetworkType.WIFI,
  isKeep: false
};

workScheduler.startWork(workInfo);

4.3 数据预取策略

在用户进入新页面前预加载数据,利用用户的阅读时间掩盖加载延迟。使用 NavPathStackonReady 回调实现:

this.navPathStack.pushPathByName("DetailPage", item).onReady(() => {
  this.preloadDetailData(item.id);
});

五、性能监控与持续优化

性能优化不是一次性工作,需要建立长效监控机制。

5.1 关键性能指标

建议监控以下指标并设置阈值告警:

指标 目标值 说明
冷启动时间 < 1.5s 从点击图标到首帧渲染完成
热启动时间 < 500ms 从后台切回前台的恢复时间
列表滑动帧率 >= 55fps 连续滑动 10 秒的平均帧率
内存占用 < 200MB 应用主进程的内存峰值
ANR 率 < 0.1% 主线程阻塞超过 5 秒的比例

5.2 性能埋点

在关键路径添加性能埋点,使用 HiLog 记录耗时:

import { hilog } from "@kit.PerformanceAnalysisKit";

const DOMAIN = 0x0001;
const TAG = "WeatherApp";

function measureTime<T>(label: string, fn: () => T): T {
  const start = Date.now();
  const result = fn();
  const cost = Date.now() - start;

  hilog.info(DOMAIN, TAG, `${label} cost: ${cost}ms`);

  if (cost > 16) {
    hilog.warn(DOMAIN, TAG, `${label} exceeds frame budget!`);
  }
  return result;
}

5.3 DevEco Studio Profiler 使用技巧

  • Trace 分析:使用 hdc shell hilog 导出系统日志,配合 Perfetto 工具分析线程调度
  • CPU Profiler:定位热点函数,识别不必要的计算
  • Layout Inspector:检查组件层级,识别冗余布局
  • Frame Timeline:可视化每一帧的耗时分布,快速定位掉帧点

六、写在最后

这半个月的性能调优使我对 HarmonyOS 5.0 的运行机制有了更深的理解。几个核心体会:

一是 TaskPool 并行能力比想象中好用。 合理使用并行任务可以显著缩短初始化时间,但也要注意不要创建过多任务导致线程竞争。

二是 ArkUI 的组件属性本身就是性能开关。 cachedCountborderRadiusobjectFit 这些看似简单的属性背后都有硬件加速支持,正确使用可以大幅减少渲染开销。

三是一定要用 DevEco Studio 的性能分析工具。 光靠感觉和肉眼观察找不到根因,Profiler 的数据驱动才是优化的正道。

四是性能优化没有银弹。 每个应用的场景不同,需要结合实际情况制定策略。但方法论是通用的:测量 → 定位 → 优化 → 验证 → 监控。

性能优化没有终点,但每次调优后看到数据变好、用户反馈变多,那种满足感是其他工作很难给予的。希望我的实战经验能帮到同样在做鸿蒙应用优化的朋友。


Logo

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

更多推荐