一、什么是性能问题和性能优化?

性能问题:应用在运行期间遇到的卡顿、白屏、动画不流畅、掉帧等影响用户使用体验的问题。

性能优化:针对出现的情况进行定位和解决。

二、为什么要做性能给你优化?

  • 提升用户体验,增加用户粘性 一个具有商业价值的App不是它的界面有多精美而是用户日活有多少
  • 应用上架通过率、市场评分以及业务转化率。直接影响App曝光度和排行,与用户下载使用量挂钩

三、如何做性能优化?

  • 检测、分析应用的性能指标、通过调试代码将指标优化到推荐区间
1. 检测工具

Code Linter 静态扫描工具

①检测代码、尤其是新增的代码 并得到解决方案

②根据提示或者官方文档修改代码,修改后再次检查 直至修复完成

如果项目中有具体的规则可以在项目根目录下的code-linter.json5中配置(规划文件名单来确定范围、规则集以及额外规则集等)

代码检测规则见 文档中心

AppAnalyzer 动态运行工具

动态检查相当于对项目以及所选模块进行一个“体检” 包含兼容性、性能以及最佳实践方案的测试。

动态检查需要真机 体检方案需要Python环境

工具会自动帮我们去检查,然后输出结果。结果中包含本次检查分数和优化方案。

修改后再次检查直至100分。

2. 分析工具
得到应用和设备的运行状态 根据运行状态发现问题 然后去制定解决方案

ArkUlInspector 界面状态分析工具

它可以用来观察当前组件的属性以及当前页面的组件结构,类似网页的F12中的元素检查器。

可以帮助我们直观地看到所有组件,找出哪些不合理的布局。

Profiler 性能实时监测工具

同样需要真机,监测设备、应用和进程等 这个工具非常丰富,不仅可以检测系统事件应用运行情况,还可以看到时间泳道中的各项精细数据,对比前后变化。还支持录制整个模版,抓取一系列的数据进行对比。

三个重要参数:

Launch 关注启动耗时

Frame 关注丢帧率和丢帧次数

Allocation 关注内存占用情况

3. 性能优化的方向
  • 启动时长的优化

启动时长是应用被拉起至应用显示页面的时间。

页面时长是页面被拉起至显示页面的执行时间。

1)减少耗时操作(冷启动回调中减少日志打印打点)

2)资源提前加载

1.网络请求优化(避免在宏任务微任务中执行网络请求)

2.Web组件加载性能优化

(利用生命周期钩子函数 预解析、预链接、预下载)JS解析JS执行绘制都需要时间

3.资源预下载(提前将大文件通过多线程到沙箱,直接读文件,避免每次都)

4.延迟加载(使用import lazy懒加载方式导入模块)

5.动态加载(在导入模式前添加await关键字修饰 只有真正调用时才去加载模块)

解释:动态加载是异步的,只有调用所依赖的模块时才会解析执行,支持运行时拼接,也就是说可以写在代码中。而延迟加载是同步的,冷启动仍然会解析遍历(只是建立依赖关系,不会立刻初始化里面代码),因为代码是从上往下执行的,所以必须在顶级导入,不支持运行拼接。

6.使用并发能力或者异步

  • 如果是网络请求这种耗时任务,同时调用多个接口,多个接口之间没有关联的,串行请求会逐个等待逐个执行,如果将请求改为并发,并加到任务队列中,这样会一次性执行完然后返回,相当于是并行请求。
  • 异步Promise.allSettled()也可以实现这样的效果

渲染丢帧的优化

表现:屏幕没有按照120帧/秒进行渲染内容,应该呈现的帧未按时呈现。

  1. 合理地控制显示隐藏 当有一个状态控制组件显隐,如果通过if/else控制组件创建与销毁,对性能非常不友好,因此可以通过.visibility这个属性控制显示隐藏而不是创建销毁,这样避免了CPU频繁绘制的压力
  2. 组件复用(使用@Reusable标记自定义组件来实现组件复用)其原理和visibility属性原理是一样的,如果自定义组件被@Reusable装饰,那么它就不会真正被销毁,而是放入缓存中复用。
  3. 懒加载(子组件迭代)使用ForEach对数据源全量渲染,大量数据会造成列表卡顿。使用LazyForEach替代ForEach,它只会创建页面可视区域的这一部分组件。
  4. 分帧渲染(displaySync.DisplaySync)这个在项目中用的并不多,它是创建一个displaySync对象,通过这个对象来订阅监听每一帧变化,这样的好处是只渲染需要的部分或者说是看到的部分,需要反复渲染的不应该被重复销毁创建而是复用。
  • 布局节点减少
  1. 如果组件不需要依赖 @Component的生命周期 我觉得可以通过自定义构建函数builder代替自定义组件。
  2. 使用合理的容器组件,Flex和Grid组件是测量、布局绘制耗时比较大的组件。非必须场景可以避免使用它们。

原因是:Flex和Grid需要根据子组件数量、尺寸约束、排列规则动态计算每个子节点的位置和大小,因此会增加 Measure测量和 Layout布局阶段的开销。

  1. 精简节点数、减少嵌套层数。

如果是嵌套层级较深的,尽可能使用相对布局来进行扁平化处理。

  • 组件绘制优化
  1. 避免在组件初始化时 aboutToAppear中做耗时任务 避免阻塞UI。
  2. 按需注册组件属性:通过attributeModifier属性,手动维护一个继承并实现AttributeModifier接口的属性管理类,通过拿到当前组件的实例对象来动态注册属性。
  3. 当容器限定其为百分比或者没有设置宽高,它的整体绘制、测量、布局都是需要比较高的时长,设置宽高后就能够减小绘制的负担,这样耗时就能大大减小。
// 导入 AttributeModifier 接口
import { AttributeModifier } from '@ohos.arkui.modifier';

// 1. 自定义属性管理类,实现 AttributeModifier 接口
//    泛型 <TextAttribute> 表示这个类是专门用来管理 Text 组件属性的
class MyTextModifier implements AttributeModifier<TextAttribute> {
  // 存放要动态修改的属性值
  backgroundColor: ResourceColor = Color.Transparent;
  fontSize: number = 14;

  // 2. 核心方法:应用普通属性
  //    参数 instance 就是当前组件的属性实例对象,可以用它来注册属性
  applyNormalAttribute(instance: TextAttribute): void {
    // 拿到组件实例后,像在链式调用里一样直接设置属性
    // 这里相当于动态地给组件加上了 .backgroundColor(...).fontSize(...)
    instance.backgroundColor(this.backgroundColor);
    instance.fontSize(this.fontSize);
  }
}

// 页面入口
@Entry
@Component
struct AttributeModifierDemo {
  // 创建属性管理类的实例,用 @State 修饰以便修改后刷新界面
  @State modifier: MyTextModifier = new MyTextModifier();

  build() {
    Column({ space: 20 }) {
      // 3. 将属性管理类绑定到组件上
      //    只要 modifier 里的属性值发生变化,applyNormalAttribute 就会被自动调用
      Text('动态属性示例')
        .attributeModifier(this.modifier)   // 关键:绑定自定义属性管理对象
        .width('80%')
        .height(60)
        .textAlign(TextAlign.Center)

      // 按钮:点击时更换背景色和字号
      Button('更换样式')
        .onClick(() => {
          // 修改 modifier 里的属性值,触发界面更新
          this.modifier.backgroundColor = Color.Orange;
          this.modifier.fontSize = 24;
        })

      // 按钮:恢复默认样式
      Button('恢复默认')
        .onClick(() => {
          this.modifier.backgroundColor = Color.Transparent;
          this.modifier.fontSize = 14;
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}
  • 实现 AttributeModifier<TextAttribute> 接口
    我们创建的 MyTextModifier 类负责管理 Text 组件的属性,泛型决定了它能操作哪种组件。
  • applyNormalAttribute(instance) 方法
    每当界面刷新(且 modifier 发生变化)时,系统会调用这个方法,并把当前 Text 组件的属性对象传进来。我们通过 instance 动态调用 .backgroundColor().fontSize() 等方法,相当于在组件上重新注册了这些属性。
  • 绑定到组件
    .attributeModifier(this.modifier) 将管理类实例和组件关联起来,后续只要改变 this.modifier 中的属性值,组件样式就会自动更新,无需手动重新创建组件或写一堆 if/else
  • 按需修改
    在按钮的 onClick 中直接修改 modifier 的字段,框架检测到变化后会自动重新执行 applyNormalAttribute,从而实现“动态注册属性”的效果。

通过这种方式,我们可以将组件的样式逻辑全部集中到一个类中,让代码更干净、更易维护,尤其适合需要频繁切换主题、动态调整样式的场景。

  • 状态刷新控制
  1. 避免不必要的状态变量使用,不一定所有的变量都需要响应式。比如不需要更新UI的数据无需状态修饰。
  2. 如果只是某一个组件使用的状态,应定义在组件内部,避免定义外层组件,导致状态变化整页刷新。
  3. 如果是多层组件嵌套,需要传递应该选择合适的装饰器做状态管理,而不是属性透传。
  4. 通过@Observed和@Track精细化拆分复杂的状态进行管理。
  5. 通过@Watch精准监听订阅精细控制组件刷新。减少状态变化而影响的范围。
  • 动画控制
  1. 使用系统提供的animation属性动画来替代自己使用定时器实现的动画。
  2. 使用图形变换属性实现组件变化(rotate、translate、scale、transform)
  3. 合理使用animateTo闭包动画,相同动画参数的动画放入一个animateTo去执行。
  4. 使用renderGroup缓存动效:可以将组件动画效果缓存起来,但是组件必须是静态组件,子组件内不能有动画效果。
  • 感知流畅优化
  1. 视觉感知优化:进入应用时添加浮层 放置动效、骨架屏 缓解用户等待时间
  2. 转场动效感知:一镜到底
  3. 合理使用动画时长感知应用流程:进度条 圆环 需要显示当前进度 填充等待时常
4. 实际项目优化方案
  • Web组件加载网页的白屏优化

        之所Web渲染的页面会在返回后再次打开时白屏,是因为WebView组件的核心机制是按需创建 离开释放。Web组件很重,里面承载了浏览器渲染的一些列的接口以及生命周期。如果每个页面都保存就会有很大的内存开销。因此每次进入Web加载的页面都会重新创建组件包括控制器一系列挂载操作。

所有操作都需要时间,所以每次用户看到都有一个白屏的阶段。

  1. 首先在整个项目的入口,也就是根节点index的aboutToAppear中通过WebviewController的initialize方法来初始化这个webView的引擎。接着在调用WebviewController中的prepareForPageLoad()方法来开启预解析,只需要传入需要提前解析的url地址,开启预链接以及配置sockets的数量即可。
  2. 预渲染主要是通过创建一个WebView的管理器,将Web组件先封装在builder中,然后手动维护一个用于动态创建节点的NodeController()和一个FrameNode节点容器,再把Web节点挂到FrameNode下面,接着初始化Web,在里面将封装的builder创建挂载到节点上。此时我会将url地址和创建的NodeController对象对应存储到字典中,作为缓存的NodeMap。最后就可以在Web组件的onAppear中调用prefetchPage预下载了。
  3. 这样测试后发现网页基本可以秒开。但是有个坑点,如果返回再次进入就空白了。其实是因为先创建好要渲染的节点,再解析页面之后,这个节点中有内容,如果内容没有发生变化,默认就不会再次渲染了。所以我记得当时解决办法是先判断,看看节点有没有内容,如果有就调用clear先清空再去预加载。
/**
 * ============================================================================
 * 文件说明:Web 预加载秒开方案
 * ============================================================================
 * 核心思路:利用 NodeController + BuilderNode + NodeContainer 实现 Web 组件的
 *          离屏预渲染,用户点击前已完成页面加载,达到"秒开"效果。
 *
 * 整体架构:
 *   1. WebData           —— Web 组件入参数据封装(url + controller)
 *   2. webBuilder        —— @Builder 函数,声明 Web 组件的 UI 结构
 *   3. WebNodeController —— 继承 NodeController,负责通过 BuilderNode 创建节点
 *   4. WebPreloadNode    —— 封装预加载节点(含 controller + nodeController)
 *   5. WebPreloadManager —— 单例管理器,统一管理预加载节点的创建与复用
 *   6. Index             —— 入口页面,包含首页列表与 Web 显示两个视图
 *
 * 关键点:
 *   - 隐藏容器:NodeContainer 宽高为 0 且 visibility=Hidden,保持 Web 节点不被销毁
 *   - 节点借用:在隐藏容器与可见容器之间切换 NodeController 引用,避免重复创建
 *   - 防白屏:返回时清空 + 重新预加载,避免内容未变化导致不渲染
 * ============================================================================
 */

// 导入所需模块
import { webview } from '@kit.ArkWeb';
import { NodeController, FrameNode, UIContext, BuilderNode } from '@kit.ArkUI';

// ============================================================================
// 0. Web 组件的入参封装类
// ============================================================================

/**
 * Web 组件的入参数据封装。
 * 将 WebviewController 和目标 URL 打包传递给 Builder,避免直接传递多个参数。
 */
class WebData {
  /** WebviewController 实例,用于控制 Web 组件的加载、刷新等操作 */
  controller: webview.WebviewController;
  /** 要加载的目标网页地址 */
  url: string;

  /**
   * 构造函数
   * @param controller - Web 控制器实例
   * @param url        - 目标网页 URL
   */
  constructor(controller: webview.WebviewController, url: string) {
    this.controller = controller;
    this.url = url;
  }
}

// ============================================================================
// 1. 定义 Web 组件的 Builder(用于动态创建节点)
// ============================================================================

/**
 * Web 组件的 @Builder 函数。
 * 该函数会被 wrapBuilder 包装后传给 BuilderNode,用于创建离屏预渲染节点
 * 以及最终显示的节点。
 *
 * @param data - 包含 url 和 controller 的 WebData 对象
 */
@Builder
function webBuilder(data: WebData) {
  // src 指定初始加载地址,controller 用于后续控制
  Web({ src: data.url, controller: data.controller })
    .domStorageAccess(true)  // 开启 DOM Storage 访问能力
    .width('100%')
    .height('100%')
}

// ============================================================================
// 2. 自定义 NodeController,负责创建 Web 的 FrameNode
// ============================================================================

/**
 * 自定义 NodeController,继承自系统 NodeController。
 * 职责:通过 BuilderNode + UIContext 创建实际的 Web FrameNode 节点,
 *       供 NodeContainer 组件渲染显示。
 */
class WebNodeController extends NodeController {
  /** 包装后的 @Builder 函数引用,用于 BuilderNode.build() 调用 */
  private builder: WrappedBuilder<[WebData]>;
  /** BuilderNode 实例(懒加载),持有实际创建的 FrameNode */
  private builderNode: BuilderNode<[WebData]> | null = null;
  /** Web 控制器实例,传递给 @Builder 中的 Web 组件 */
  private controller: webview.WebviewController;
  /** 目标网页 URL,传递给 @Builder 中的 Web 组件 */
  private url: string;

  /**
   * 构造函数
   * @param controller - Web 控制器实例
   * @param url        - 目标网页 URL
   */
  constructor(controller: webview.WebviewController, url: string) {
    super();
    this.controller = controller;
    this.url = url;
    // 将全局 @Builder 函数包装为 WrappedBuilder,供 BuilderNode 使用
    this.builder = wrapBuilder(webBuilder);
  }

  /**
   * 核心方法(由 NodeContainer 触发):使用 UIContext 创建实际的 FrameNode 节点。
   * 采用懒加载策略:首次调用时创建 BuilderNode 并构建内容,后续直接返回已创建的节点。
   *
   * @param uiContext - 当前 UI 上下文,BuilderNode 需要它来创建节点
   * @returns 创建的 FrameNode,失败返回 null
   */
  makeNode(uiContext: UIContext): FrameNode | null {
    // 懒加载:仅在首次调用时创建 BuilderNode
    if (this.builderNode === null) {
      this.builderNode = new BuilderNode(uiContext);
      // 使用 builder 和 WebData 参数构建 Web 组件内容
      this.builderNode.build(this.builder, new WebData(this.controller, this.url));
    }
    // 返回已创建的 FrameNode
    if (this.builderNode) {
      return this.builderNode.getFrameNode();
    }
    return null;
  }
}

// ============================================================================
// 3. 预加载节点封装
// ============================================================================

/**
 * 预加载节点封装类。
 * 将 WebNodeController 和 WebviewController 打包在一起,
 * 同时提供"清空 + 重新预加载"能力以解决返回白屏问题。
 */
class WebPreloadNode {
  /** 节点控制器,传递给 NodeContainer 用于渲染 Web 内容 */
  public nodeController: WebNodeController;
  /** Web 控制器,直接操作 Web 组件的加载、预取等行为 */
  public webController: webview.WebviewController;
  /** 目标网页 URL */
  public url: string;

  /**
   * 构造函数:根据 URL 创建 WebviewController 和 WebNodeController
   * @param url - 目标网页 URL
   */
  constructor(url: string) {
    this.url = url;
    this.webController = new webview.WebviewController();
    this.nodeController = new WebNodeController(this.webController, url);
  }

  /**
   * 关键方法:解决"返回再进入页面时白屏"的问题。
   *
   * 白屏原因:Web 内容未变化时,NodeContainer 可能不会重新触发渲染。
   *
   * 解决方案:
   *   1. 先加载 about:blank 清空当前内容
   *   2. 调用 prefetchPage 预取目标页面(利用已初始化的 Web 引擎)
   *   3. 再加载真实 URL,触发重新渲染
   */
  clearAndPreload(): void {
    try {
      // 第一步:清空当前内容,避免因内容未变化导致 NodeContainer 不重新渲染
      this.webController.loadUrl('about:blank');
      // 第二步:预取目标页面,利用已初始化的 Web 引擎提前下载资源
      this.webController.prefetchPage(this.url);
      // 第三步:加载真实页面,触发完整渲染
      this.webController.loadUrl(this.url);
    } catch (err) {
      console.error(`clearAndPreload error: ${JSON.stringify(err)}`);
    }
  }
}

// ============================================================================
// 4. 预渲染管理器(单例)
// ============================================================================

/**
 * 预渲染管理器(单例模式)。
 * 职责:
 *   - 统一管理预加载节点的创建、复用与获取
 *   - 提供 doPreload 方法在隐藏容器挂载后执行真正的预加载
 *
 * 单例设计确保整个应用生命周期内只有一个管理器实例,
 * 避免重复创建多个预渲染节点浪费资源。
 */
class WebPreloadManager {
  /** 单例实例 */
  private static instance: WebPreloadManager;
  /** 当前持有的预加载节点(URL 相同时复用) */
  private preloadNode: WebPreloadNode | null = null;

  /**
   * 获取单例实例(懒加载)
   * @returns WebPreloadManager 单例
   */
  static getInstance(): WebPreloadManager {
    if (!WebPreloadManager.instance) {
      WebPreloadManager.instance = new WebPreloadManager();
    }
    return WebPreloadManager.instance;
  }

  /**
   * 创建预加载节点。如果已存在节点且 URL 相同则直接复用,避免重复创建。
   * @param url - 目标网页 URL
   * @returns 预加载节点实例
   */
  createPreloadNode(url: string): WebPreloadNode {
    // 不存在节点,或 URL 变化时,创建新节点
    if (!this.preloadNode || this.preloadNode.url !== url) {
      this.preloadNode = new WebPreloadNode(url);
    }
    return this.preloadNode;
  }

  /**
   * 获取当前预加载节点
   * @returns 预加载节点,未创建时返回 null
   */
  getPreloadNode(): WebPreloadNode | null {
    return this.preloadNode;
  }

  /**
   * 在隐藏容器挂载后调用,执行真正的预加载。
   * 此方法触发 prefetchPage(预取页面资源)和 loadUrl(加载页面),
   * 让 Web 引擎在用户点击前就完成页面下载与渲染。
   *
   * @param node - 要执行预加载的节点
   */
  doPreload(node: WebPreloadNode): void {
    try {
      // 预取页面资源,提前下载 HTML/JS/CSS 等
      node.webController.prefetchPage(node.url);
      // 加载页面,触发渲染(在隐藏容器中完成,用户不可见)
      node.webController.loadUrl(node.url);
    } catch (err) {
      console.error(`doPreload error: ${JSON.stringify(err)}`);
    }
  }
}

// ============================================================================
// 5. 入口页面
// ============================================================================

/**
 * 入口页面组件。
 *
 * 页面结构:
 *   - 首页视图(showWeb=false):标题 + "打开网页"按钮
 *   - Web视图(showWeb=true) :返回按钮 + NodeContainer 显示 Web 内容
 *   - 隐藏容器(始终在布局中):宽高为 0 且不可见,保持 Web 节点活跃
 *
 * 核心交互流程:
 *   1. aboutToAppear:初始化引擎 → 预连接 → 创建预渲染节点放入隐藏容器
 *   2. onPageShow:隐藏容器挂载后执行预加载(提前下载页面)
 *   3. 点击"打开网页":清空+重新预加载 → 节点从隐藏容器借到可见容器
 *   4. 点击"返回":节点从可见容器还给隐藏容器(不被销毁)
 */
@Entry
@Component
struct Index {
  /** 是否显示 Web 视图(false=首页列表,true=Web 内容) */
  @State showWeb: boolean = false;
  /** 当前可见区域的节点控制器(用于 Web 显示界面的 NodeContainer) */
  @State visibleController: WebNodeController | null = null;
  /** 隐藏容器中的节点控制器(保持 Web 节点活跃,不被销毁) */
  @State hiddenController: WebNodeController | null = null;

  /** 预加载节点引用(包含 controller + nodeController) */
  private preloadNode: WebPreloadNode | null = null;
  /** 预渲染管理器单例 */
  private webManager: WebPreloadManager = WebPreloadManager.getInstance();

  /**
   * 组件生命周期:aboutToAppear
   * 在组件创建后、build() 执行前调用,用于初始化数据。
   *
   * 执行步骤:
   *   1. 初始化 Web 引擎(应用生命周期内只需一次)
   *   2. 预连接目标 URL(DNS 解析 + TCP/TLS 握手)
   *   3. 创建预渲染节点并放入隐藏容器
   */
  aboutToAppear(): void {
    // 步骤1:初始化浏览器引擎(整个应用生命周期只需调用一次)
    webview.WebviewController.initializeWebEngine();

    // 步骤2:预解析目标 URL,提前进行 DNS 解析、TCP/TLS 握手等
    try {
      // 参数:url, 是否预连接, socket 数量
      webview.WebviewController.prepareForPageLoad('https://www.suning.com', true, 3);
    } catch (err) {
      console.error(`prepareForPageLoad error: ${JSON.stringify(err)}`);
    }

    // 步骤3:创建预渲染节点并暂存到隐藏容器
    this.preloadNode = this.webManager.createPreloadNode('https://www.suning.com');
    this.hiddenController = this.preloadNode.nodeController;
  }

  /**
   * 组件生命周期:onPageShow
   * 页面显示时调用。此时隐藏容器中的 Web 节点已挂载到布局中,
   * 可以安全地执行预加载(prefetchPage + loadUrl)。
   */
  onPageShow(): void {
    // 隐藏容器中的 Web 节点已挂载,执行预下载
    if (this.preloadNode) {
      this.webManager.doPreload(this.preloadNode);
    }
  }

  /**
   * 构建 UI 布局。
   *
   * 使用 Stack 层叠布局,包含三个互斥/共存的部分:
   *   - if (!showWeb) : 首页列表视图(按钮触发跳转)
   *   - else          : Web 显示视图(按钮触发返回)
   *   - 隐藏容器      : 始终存在于布局中,宽高为 0 且不可见
   */
  build() {
    Stack() {
      // ========== 首页列表界面 ==========
      if (!this.showWeb) {
        Column({ space: 20 }) {
          Text('首页')
            .fontSize(30)
            .fontWeight(FontWeight.Bold)
          Button('打开网页(秒开)')
            .onClick(() => {
              // 获取预加载节点
              let node = this.webManager.getPreloadNode();
              if (node) {
                // 核心:清空 + 重新预加载,解决返回白屏问题
                node.clearAndPreload();
                // 将节点从隐藏容器"借"到可见容器
                this.visibleController = node.nodeController;
                // 隐藏容器置空(同一节点不能同时被两个 NodeContainer 引用)
                this.hiddenController = null;
                // 切换到 Web 显示视图
                this.showWeb = true;
              }
            })
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }
      // ========== Web 显示界面 ==========
      else {
        Column() {
          Button('返回')
            .onClick(() => {
              // 将节点"还"给隐藏容器,保证其不被销毁
              this.hiddenController = this.visibleController;
              // 可见容器置空
              this.visibleController = null;
              // 切换回首页列表视图
              this.showWeb = false;
            })
          // 可见的 Web 容器:渲染预加载好的 Web 内容
          if (this.visibleController) {
            NodeContainer(this.visibleController)
              .width('100%')
              .height('90%')
          }
        }
        .width('100%')
        .height('100%')
      }

      // ========== 隐藏容器 ==========
      // 一直存在于布局中,但宽高为 0 且不可见。
      // 作用:保持 Web 节点活跃,避免被系统销毁,实现秒开。
      if (this.hiddenController) {
        NodeContainer(this.hiddenController)
          .width(0)
          .height(0)
          .visibility(Visibility.Hidden)
      }
    }
  }
}
  • 渲染丢帧优化方案
  1. 创建一个displaySync的实例对象 displaySync.create()
  2. 通过setExpectedFrameRateRange设置可变帧率。在这里可以设置预计值、最小值和最大值(一般最小值给60,因为大于60肉眼无法分辨出来)。
  3. 通过实例对象中的on方法监听帧率的变化,第一个参数是'frame'类型,第二个参数是一个回调函数,可以在回调函数中监听第一帧、第二帧、更多帧,但是这个计数count不会自增,所以每次监听后需要+1来到下一帧。
  4. 如果要取消监听,通过实例对象的stop和off方法,传入'frame'即可
  5. 准备工作做好后,就是实际使用了,这里会根据业务需求,需要多次测试,这里也是最麻烦的。

因为在业务中,接口返回的数据一般是一个数组,而不是一条数据。所以我们需要根据具体业务情况进行规划渲染和拆分。以及拆分一些静态内容也需要把这个帧率计算排列的位置空出来,确保下一帧可以按照我们期待的业务需求去渲染。这个过程是非常复杂的,需要精细化拆分业务,包括这一帧渲染内容、下一帧渲染内容。需要多次反复测试,利用Profiler分析每一次变化,观察泳道的丢帧情况。例如,第一帧丢帧就需要把第一帧拆分更加精细,因为涉及到冷启动除了渲染页面还有很多事情要做,这些都是造成丢帧的元素。

长列表优化方案:将ForEach替换为LazyForEach的话,首先要准备一个数据源类,需要这个类继承并实现(implements) IDataSource接口。然后需要实现四个方法:totalCount、getData、registerDataChangeListener和unregisterDataChangeListener。要实现这些方法,还需要准备数据数组和监听器数组。以及自己操作数据源数组的方法 如pushData。在页面中,首先声明一个当前数据源类实例化出来的数据源对象。LazyForEach组件接收三个参数,第一个是数据源对象,传入即可,第二个是子组件迭代函数,第三个是键值生成函数。这些和ForEach使用方式一样。只是操作数组的方法有一点不同,需要通过调用数据源类的方法去实现。

创建display实例:displayAbi = displaySync.create()

设置可变帧率:displayAbi.setExpectFrameRareRange()

监听帧变化:displayAbi.on()

开启监听:displayAbi.start()

更新帧:停止和取消监听: displayAbi.stop()

5. 内存优化方向及内存泄漏

目的:降低应用运行时内存占用,避免内存泄漏、溢出等问题

内存泄漏:本质是内存只占不还,由于对象已不再使用,但因被强引用而无法被GC回收,内存占用呈现持续上升趋势。

由于订阅监听没有取消,页面组件及上下文被全局对象持有无法回收。

定时器没有在页面销毁时清除,导致回调函数持有页面this引用。

长生命周期对象AppStorage、全局单例持有了短生命周期对象或组件的闭包或回调。

Native层申请了C++侧内存或创建了napi_ref跨作用域延长和管理 JavaScript 对象的生命周期,但没有手动释放。

内存溢出:本质是内存不够用了,应用占用的内存超过了系统分配给该应用/进程的上限,直接会导致应用crash崩溃退出。

一次性解码并加载多张超高分辨率图片到内存中,导致进程内存限制被越界。

在for循环或频繁触发的UI更新中大量创建临时大数组、缓冲区,冲出ArrayBuffer

用户反复打开关闭有内存泄漏的页面,内存逐步累积,最终触顶内存溢出阈值

内存优化方向:

  1. 避免内存泄漏:避免声明变量始终处于可访达的状态,影响垃圾回收机制回收它们。
  2. 使用LRUcache缓存,它可以设置最大缓存数量,当超过这个数量后,会根据最近最少使用原则去覆盖之前存储的内容,内存上限是固定的。
  3. 合理使用图片尺寸。
  4. 预置图片加载优化:"compression"开启纹理压缩,配置纹理超压缩。内存占用下降 图片渲染时间增加。
  5. 资源释放优化,在使用涉及到媒体功能的录音机、播音播放器等组件,在使用完毕后要及时停止和释放资源。
  6. 在UIAbility生命周期回调onMemoryLevel监听内存等级,对不重要的资源进行内存释放、停止非核心任务以及释放不影响程序运行的内存。

使用LRU来缓存对象,可以避免重新计算、重新加载以及重新创建对象。

核心思想是 当存储进来后,如果缓存容量不足时,优先覆盖掉最长时间没有被访问的数据

底层逻辑:HashMap+双向链表

HashMap负责快速查找,双向链表负责排序

时间复杂度:是用来衡量一个算法随着输入规模增长,执行时间增长速度的指标。它不是计算具体执行多少秒,而是关注:当数据量越来越大时,算法性能会怎么变化。

O(1)常数时间、O(n)线性增长、 O(n²)平方复杂度、O(log n)对数复杂度

附:

高性能编程常见问题:

  1. 尽量使用const声明常量
  2. 减少使用嵌套 export */import *的方式
  3. 减少全局变量存储,使用局部变量提升属性访问速度
  4. 防止传感器滥用
  5. 减少不必要的后台任务,防止抢占资源
  6. 防止算法时间复杂度失控
  7. 清理冗余文件和代码
  8. 使用HSP代替HAR
  9. 依赖管理优化,删除无用依赖
Logo

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

更多推荐