1 点击完成时延概述

1.1 什么是点击完成时延?

点击完成时延是指从用户手指触摸屏幕开始,到所有与点击相关的任务完全执行完毕所经过的时间。这与点击响应时延(只需视觉反馈)不同,完成时延要求所有关联操作都执行完成。

1.2 时延组成阶段

一次完整的点击完成包含以下阶段:

  1. 输入处理阶段:触摸检测和事件传递(0-50ms)

  2. 视觉反馈阶段:界面立即响应(50-100ms)

  3. 业务处理阶段:数据加载、计算处理(100-500ms+)

  4. 内容呈现阶段:完整内容展示和可交互(500ms+)

2 前端点击完成时延分析

2.1 前端完整事件处理流程

在前端开发中,点击完成的完整流程如下:

触摸开始 → 事件处理 → 视觉反馈 → 异步任务 → 数据加载 → 内容渲染 → 可交互状态

2.2 分析工具与方法

2.2.1 使用Performance API进行全面监控
// 全面监控点击完成时间
function trackClickCompletion(element, eventName) {
  const startTime = performance.now();
  
  element.addEventListener('click', function() {
    // 记录点击时间
    performance.mark(`${eventName}_click_start`);
    
    // 监听所有异步任务完成
    Promise.allSettled([
      loadData(),
      updateUI(),
      sendAnalytics()
    ]).then(() => {
      const completionTime = performance.now() - startTime;
      performance.mark(`${eventName}_completion_end`);
      performance.measure(
        `${eventName}_completion_time`,
        `${eventName}_click_start`,
        `${eventName}_completion_end`
      );
      
      console.log(`点击完成时延: ${completionTime}ms`);
      reportToAnalytics(completionTime);
    });
  });
}
2.2.2 监控资源加载时间
// 监控所有相关资源加载
function monitorResourceLoading() {
  const resources = [];
  
  // 监听资源加载
  const observer = new PerformanceObserver((list) => {
    list.getEntries().forEach((entry) => {
      resources.push({
        name: entry.name,
        duration: entry.duration,
        startTime: entry.startTime
      });
    });
  });
  
  observer.observe({entryTypes: ['resource']});
  
  return resources;
}

2.3 常见问题与优化策略

2.3.1 异步任务管理优化

问题:多个异步任务未有效协调,导致完成时间延长。

// 优化前:顺序执行异步任务
async function handleClick() {
  await loadUserData();    // 200ms
  await loadProductInfo(); // 150ms
  await loadComments();    // 100ms
  // 总耗时: 450ms
}

// 优化后:并行执行+优先级调度
async function handleClick() {
  // 高优先级任务立即执行
  const uiUpdate = updateUI();
  
  // 中优先级任务并行执行
  const userData = loadUserData();
  const productInfo = loadProductInfo();
  
  // 低优先级任务延迟执行
  const lowPriorityTasks = Promise.allSettled([
    loadComments(),
    sendAnalytics(),
    preloadRelatedContent()
  ]);
  
  // 等待关键任务完成
  await Promise.all([uiUpdate, userData, productInfo]);
  
  // 标记主要任务完成
  markCompletion();
  
  // 继续执行低优先级任务
  lowPriorityTasks.then(() => {
    markFullCompletion();
  });
}
2.3.2 数据加载与缓存策略
// 智能预加载与缓存
class DataManager {
  constructor() {
    this.cache = new Map();
    this.pendingRequests = new Map();
  }
  
  async loadWithCache(key, loader, priority = 'normal') {
    // 检查缓存
    if (this.cache.has(key)) {
      return this.cache.get(key);
    }
    
    // 检查是否已在加载中
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key);
    }
    
    // 根据优先级调度
    const promise = this.executeWithPriority(loader, priority);
    this.pendingRequests.set(key, promise);
    
    try {
      const result = await promise;
      this.cache.set(key, result);
      return result;
    } finally {
      this.pendingRequests.delete(key);
    }
  }
  
  executeWithPriority(loader, priority) {
    switch(priority) {
      case 'high':
        return Promise.resolve().then(loader);
      case 'low':
        return new Promise(resolve => {
          if ('requestIdleCallback' in window) {
            requestIdleCallback(() => resolve(loader()));
          } else {
            setTimeout(() => resolve(loader()), 0);
          }
        });
      default:
        return Promise.resolve().then(loader);
    }
  }
}

3 鸿蒙应用点击完成时延分析

3.1 鸿蒙完整事件处理机制

鸿蒙应用的点击完成流程涉及多个线程协同:

UI线程:事件接收 → 状态更新 → 界面渲染
Worker线程:数据处理 → 网络请求 → 文件操作
渲染服务:布局计算 → 动画执行 → 最终呈现

3.2 分析工具与方法

3.2.1 使用HiTrace进行全面跟踪
// 全链路跟踪点击完成时延
import hiTraceMeter from '@ohos.hiTraceMeter';
import worker from '@ohos.worker';

@Component
struct ProductDetailPage {
  @State productData: Product | null = null;
  @State relatedItems: RelatedItem[] = [];
  @State isLoading: boolean = false;
  
  async onProductClick(productId: string) {
    // 开始全链路跟踪
    const traceId = hiTraceMeter.startTrace('product_click_completion', 0);
    
    try {
      this.isLoading = true;
      
      // 并行执行关键任务
      await Promise.all([
        this.loadProductData(productId, traceId),
        this.loadRelatedItems(productId, traceId),
        this.updateUserHistory(productId, traceId)
      ]);
      
      // 标记主要任务完成
      hiTraceMeter.finishTrace('product_click_completion', traceId);
      
    } finally {
      this.isLoading = false;
    }
    
    // 后台继续执行次要任务
    this.executeBackgroundTasks(productId);
  }
  
  async loadProductData(productId: string, traceId: number) {
    hiTraceMeter.startTrace('load_product_data', traceId);
    try {
      this.productData = await productService.getProductDetails(productId);
    } finally {
      hiTraceMeter.finishTrace('load_product_data', traceId);
    }
  }
  
  // 其他加载方法...
}
3.2.2 多线程性能监控
// 监控多线程任务执行
class ThreadPerformanceMonitor {
  private mainThreadTasks: Map<string, number> = new Map();
  private workerTasks: Map<string, number> = new Map();
  
  startMainThreadTask(taskName: string) {
    this.mainThreadTasks.set(taskName, Date.now());
  }
  
  endMainThreadTask(taskName: string) {
    const startTime = this.mainThreadTasks.get(taskName);
    if (startTime) {
      const duration = Date.now() - startTime;
      this.reportPerformance('main_thread', taskName, duration);
    }
  }
  
  monitorWorkerTask(worker: worker.ThreadWorker, taskName: string) {
    const startTime = Date.now();
    
    worker.onmessage = () => {
      const duration = Date.now() - startTime;
      this.reportPerformance('worker_thread', taskName, duration);
    };
  }
}

3.3 常见问题与优化策略

3.3.1 多线程任务协调优化

问题:线程间通信开销大,任务调度不合理。

// 优化线程间任务调度
class TaskScheduler {
  private mainThreadQueue: Task[] = [];
  private workerQueue: Task[] = [];
  private isProcessing = false;
  
  // 添加任务到相应队列
  addTask(task: Task, priority: 'high' | 'normal' | 'low' = 'normal') {
    if (task.type === 'ui_update') {
      this.addToMainThreadQueue(task, priority);
    } else {
      this.addToWorkerQueue(task, priority);
    }
  }
  
  // 智能调度任务执行
  private async processQueues() {
    if (this.isProcessing) return;
    
    this.isProcessing = true;
    
    // 优先处理UI任务
    while (this.mainThreadQueue.length > 0) {
      const task = this.mainThreadQueue.shift();
      await this.executeMainThreadTask(task!);
    }
    
    // 然后处理Worker任务
    while (this.workerQueue.length > 0) {
      const task = this.workerQueue.shift();
      await this.executeWorkerTask(task!);
    }
    
    this.isProcessing = false;
  }
}
3.3.2 内存与资源管理优化
// 智能资源管理
class ResourceManager {
  private static instance: ResourceManager;
  private cache: Map<string, any> = new Map();
  private preloadQueue: string[] = [];
  
  // 预加载可能需要的资源
  preloadResources(resourceIds: string[]) {
    resourceIds.forEach(id => {
      if (!this.cache.has(id) && !this.preloadQueue.includes(id)) {
        this.preloadQueue.push(id);
      }
    });
    
    // 在空闲时预加载
    this.schedulePreload();
  }
  
  private schedulePreload() {
    if (this.preloadQueue.length === 0) return;
    
    // 使用空闲时间预加载
    if (typeof requestIdleCallback !== 'undefined') {
      requestIdleCallback(() => {
        this.executePreload();
      });
    } else {
      setTimeout(() => this.executePreload(), 1000);
    }
  }
  
  private async executePreload() {
    while (this.preloadQueue.length > 0) {
      const resourceId = this.preloadQueue.shift()!;
      try {
        const resource = await this.loadResource(resourceId);
        this.cache.set(resourceId, resource);
      } catch (error) {
        console.warn(`预加载资源失败: ${resourceId}`, error);
      }
    }
  }
}

4 跨平台对比与最佳实践

4.1 前端与鸿蒙的完成时延差异

方面 Web前端 鸿蒙应用
线程模型 主线程+有限Web Worker 多线程架构,更强隔离性
资源管理 浏览器缓存机制 系统级资源管理
性能工具 浏览器DevTools DevEco Studio完整工具链
优化手段 代码分割、懒加载 多线程协调、资源预加载

5 实战案例:电商应用优化实践

5.1 案例背景

某电商应用商品详情页点击完成时延过长,用户从点击商品到完全加载平均需要2.5秒。

5.2 优化实施过程

5.2.1 问题分析

通过性能分析发现主要瓶颈:

  1. 串行加载商品数据、评论、推荐列表

  2. 图片加载阻塞主要内容渲染

  3. 过多的同步布局计算

5.2.2 优化方案实施
// 优化后的商品详情页加载逻辑
@Component
struct OptimizedProductDetail {
  @State product: Product | null = null;
  @State comments: Comment[] = [];
  @State recommendations: Product[] = [];
  @State imagesLoaded: boolean = false;
  
  async onProductClick(productId: string) {
    // 1. 立即显示骨架屏
    this.showSkeleton();
    
    // 2. 并行加载关键数据
    const [productData, initialComments] = await Promise.all([
      this.loadProductData(productId),
      this.loadInitialComments(productId)
    ]);
    
    this.product = productData;
    this.comments = initialComments;
    
    // 3. 标记主要内容已加载完成
    this.markContentReady();
    
    // 4. 后台加载次要内容
    this.loadSecondaryContent(productId);
  }
  
  async loadSecondaryContent(productId: string) {
    // 低优先级任务
    Promise.allSettled([
      this.loadMoreComments(productId),
      this.loadRecommendations(productId),
      this.preloadRelatedProducts(productId)
    ]).then(() => {
      this.markFullyLoaded();
    });
  }
  
  // 图片加载优化
  async loadImagesOptimized(images: string[]) {
    const criticalImages = images.slice(0, 3); // 优先加载前3张图片
    const otherImages = images.slice(3);
    
    // 优先加载关键图片
    await this.preloadImages(criticalImages);
    this.imagesLoaded = true;
    
    // 延迟加载其他图片
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        this.preloadImages(otherImages);
      });
    }
  }
}
5.2.3 数据缓存策略优化
// 多级缓存策略
class MultiLevelCache {
  private memoryCache: Map<string, any> = new Map();
  private persistentCache: any = null;
  
  async getWithCache(key: string, loader: () => Promise<any>, options: CacheOptions = {}) {
    // 1. 检查内存缓存
    if (this.memoryCache.has(key)) {
      return this.memoryCache.get(key);
    }
    
    // 2. 检查持久化缓存
    if (this.persistentCache) {
      const cached = await this.persistentCache.get(key);
      if (cached) {
        // 回填内存缓存
        this.memoryCache.set(key, cached);
        return cached;
      }
    }
    
    // 3. 加载新数据
    const data = await loader();
    
    // 4. 更新缓存
    this.memoryCache.set(key, data);
    if (this.persistentCache && options.persist) {
      await this.persistentCache.set(key, data, options.ttl);
    }
    
    return data;
  }
}

5.3 优化成果

经过上述优化措施:

  • 平均点击完成时延:从2500ms降低到800ms

  • 90分位时延:从4000ms降低到1200ms

  • 用户交互满意度:提升35%

  • 转化率:提升18%

6 鸿蒙点击完成时延实操

完成时延优化概述

在移动终端应用开发中,完成时延指用户从发出触控指令到界面完全刷新并达到可读稳定状态的时间。点击完成时延分为页面内跳转和页面间跳转两种类型。完成时延在用户体验设计中至关重要,直接影响用户对产品的满意度和使用体验。完成时延反映了用户对响应速度的整体感受,影响触控交互的及时性和愉悦性。如图一所示,点击完成时延包含点击响应时延。有关响应时延阶段的优化分析,请参考《点击响应时延分析》

在一定时延水平以上,完成时延越短越好,但在达到一定阈值后,用户的流畅体验不会继续提升。建议应用或元服务内点击操作完成时延≤900ms。更多体验建议,请参考指南《时延体验建议》。本文将介绍相关分析工具、点击完成时延问题定位流程及常见问题根因分析。

图1 点击完成起止点示意图

图2 页面转场过程解析

工具介绍

性能问题检测工具 AppAnalyzer

AppAnalyzer是DevEco Studio提供的检测评分工具,用于测试和评估HarmonyOS应用或元服务的质量,快速提供评估结果和改进建议。当前支持的测试类型包括兼容性、性能、UX测试和最佳实践。点击完成时延是性能测试中的一项检测规则,开发者可以使用该工具检测响应性能。

具体使用可参考《AppAnalyzer》

性能问题分析工具 DevEco Profiler

性能调优深入分析工具,支持冷启动、卡顿丢帧、状态变量、并行化、网络耗时、ArkWeb、内存优化等场景化调优能力。其中Frame分析可以帮助开发者深度分析性能问题,通过录制应用运行过程中的关键数据,从而识别卡顿丢帧、耗时长等问题的原因所在。

具体使用可参考《DevEco Profiler》

性能问题分析工具 ArkUI Inspector

ArkUI Inspector是DevEco Studio中提供用于检查UI的工具,开发者可以借助它预览真机或模拟器中的UI效果,快速定位布局层级问题,也可以观察组件属性、不同组件之间的关系等。

具体使用可参考《ArkUI Inspector》

问题定位流程

下图展示了定位点击完成时延高耗时的简易流程

图3 问题定位流程图

如上图所示,分析点击完成时延问题通常需要以下步骤:

  1. 性能体检:使用性能检测工具AppAnalyzer检测应用是否存在性能问题。
  2. 确定完成时延:使用录屏工具确定点击完成时延的起点与终点,计算整个完成时延的耗时,判断是否符合《时延体验建议》中的规范。
  3. 抓取Trace信息:使用性能分析工具DevEco Profiler抓取Trace,并确定Trace图中的起止点。
  4. 分析问题:结合关键泳道Trace信息以及ArkUI Inspector布局分析工具来定位具体问题。

关键泳道简介

上述五个关键泳道可从函数调用耗时、转场页面绘制耗时和转场动画时延三个角度进行分析。接下来,将依据这三个角度对关键泳道进行详细介绍

  • 函数调用耗时分析:

    ArkTS Callstack:提供ArkTS侧的方法调用栈信息,用于分析ArkTS代码执行和性能瓶颈;

    Callstack:提供Native侧的方法调用栈信息,用于分析Native层面性能问题;

  • 转场页面绘制耗时分析:

    Frame:提供应用主线程的帧渲染信息,帮助识别未按时渲染的帧及其原因;

    ArkUI Component:提供ArkUI组件的创建、布局、渲染等详细信息,帮助识别耗时较长的组件;

  • 转场动画时延分析:

    H:Animator:提供动画执行过程中的详细信息,帮助识别转场动画是否耗时较长;

    关键Trace说明如下

    序号

    泳道

    Trace点

    描述

    1

    应用线程

    ReceiveVsync

    接受Vsync信号

    2

    应用线程

    OnvsyncEvent

    收到Vsync信号,渲染流程开始

    3

    应用线程

    FlushVsync

    刷新视图同步事件,包括记录帧信息、刷新任务、绘制上下文、处理用户输入

    4

    应用线程

    FlushDirtyNodeUpdate

    标脏组件刷新。页面刷新渲染的时候要尽量减少刷新的组件数量。当状态变量改变后,会先对状态变量相关的组件进行标脏,然后对这些组件重新测量和布局,最后再进行渲染

    5

    应用线程

    JSAnimation

    显示动画,动画会影响组件加载完成时延

    6

    应用线程

    FlushLayoutTask

    执行布局任务。在此阶段会对组件做布局测算,如果层级较深或者组件较多会影响性能

    7

    应用线程

    FlushMessages

    发送消息通知图形侧进行渲染

    8

    应用线程

    aboutToBeDeleted

    自定义组件生命周期函数,组件析构时出现,在未使用复用机制时,FlushDirtyNodeUpdate和LazyForEach predict下会析构组件,导致刷新时组件重复创建

    9

    应用进程

    SendCommands

    应用UI提交到Render Service

    10

    ArkTS Callstack

    createHttp

    创建网络请求

    11

    ArkTS Callstack

    request

    发送网络请求

    12

    ArkTS Callstack

    parse

    解析数据

    13

    ArkTS Callstack

    off

    取消订阅

关键Trace分析

确定起止点

开发者可以使用录屏辅助测试,通过录屏分析工具确定点击完成时延的起止点,从而判断是否存在需要优化的时延问题。

DevEco Profiler工具分使用方式可以参考Frame分析。下面介绍如何使用DevEco Profiler工具确定点击完成时延Trace的起止点。

  1. 搜索"H:DispatchTouchEvent"标签,找到type=1的那个DispatchTouchEvent,就是点击离手起点,将该时间戳设为起点。

    图4 确认Trace起点

  2. 点击操作完成时延的终点位置在泳道图中没有明确的Trace点,需要通过录屏工具计算出完成时延的耗时时间。从起点往后拉相同的时间找到终点位置。

    图5 确认Trace终点

  3. 使用Profiler工具标记Trace起点与终点。

ArkTS Callstack泳道分析ArkTS侧耗时函数

在ArkTS Callstack子泳道中,ArkVM是需要优先查看耗时情况的泳道,可以观察ArkTS侧方法的耗时。优先分析耗时最长的调用栈(program除外,program表示程序执行进入纯Native代码阶段,该阶段无ArkTS代码执行,也无ArkTS调用Native或Native调用ArkTS的情况,需要切换到Callstack泳道查看具体的调用栈信息,通常难以通过这里分析出有效信息)。逐级展开,可以看到具体耗时的文件。基于 “HMOS世界”切换tab页场景,抓取Trace信息。

图6 ArkTS Callstack泳道图

观察发现MainPage文件中匿名函数耗时350ms,展开该节点。

图7 ArkTS Callstack泳道耗时函数详情

展开节点后发现函数调用链中AudioPlayerService中getInstance函数调用耗时327ms,接下来定位源代码。


// products\phone\src\main\ets\pages\MainPage.ets

Tabs({ index: this.currentIndex }) {
// ...
}
.layoutWeight(1)
.barHeight(0)
.scrollable(false)
.onChange((index) => {
this.currentIndex = index;
ContinueModel.getInstance().data.mainTabIndex = index;
if (AppStorage.get('audioPlayerStatus') !== AudioPlayerStatus.IDLE) {
AudioPlayerService.getInstance().stop().then(() => {
AudioPlayerService.destroy();
});
}
})

MainPage.ets

AudioPlayerService.ets相关代码如下

// commons\audioplayer\src\main\ets\service\AudioPlayerService.ets
export class AudioPlayerService {
private static instance: AudioPlayerService | null = null;
// ...
public static getInstance(): AudioPlayerService {
if (!AudioPlayerService.instance) {
AudioPlayerService.instance = new AudioPlayerService();
}
return AudioPlayerService.instance;
}
public static destroy(): void {
if (AudioPlayerService.instanceIsNotNull()) {
AudioPlayerService.getInstance().releaseAudioPlayer();
AudioPlayerService.instance = null;
}
}
// ...
}

AudioPlayerService.ets

观察源代码发现AudioPlayerService调用getInstance创建单例对象耗费大量时间,随即又调用destroy方法销毁对象。优化方式如下:获取单例对象前,先判断单例对象是否被实例化,若没有实例化则直接跳过获取与销毁,避免实例对象的无效创建与销毁,参考如下代码。


// products\phone\src\main\ets\pages\MainPage.ets

Tabs({ index: this.currentIndex }) {
// ...
}
.layoutWeight(1)
.barHeight(0)
.scrollable(false)
.onChange((index) => {
this.currentIndex = index;
ContinueModel.getInstance().data.mainTabIndex = index;
if (AppStorage.get('audioPlayerStatus') !== AudioPlayerStatus.IDLE) {
AudioPlayerService.getInstance().stop().then(() => {
AudioPlayerService.destroy();
});
}
})

MainPage.ets

优化后AudioPlayerService.ets代码如下:


// commons\audioplayer\src\main\ets\service\AudioPlayerService.ets
export class AudioPlayerService {
private static instance: AudioPlayerService | null = null;
// ...
public static getInstance(): AudioPlayerService {
if (!AudioPlayerService.instance) {
AudioPlayerService.instance = new AudioPlayerService();
}
return AudioPlayerService.instance;
}
public static destroy(): void {
if (AudioPlayerService.instanceIsNotNull()) {
AudioPlayerService.getInstance().releaseAudioPlayer();
AudioPlayerService.instance = null;
}
}
public static instanceIsNotNull(): boolean {
return AudioPlayerService.instance !== null;
}
// ...
}

AudioPlayerService.ets

Frame主线程泳道分析异常帧

查看Frame泳道中的应用主线程子泳道,观察app侧帧数据。如果在这个泳道中出现红色帧,通常表示该帧的渲染时间超过了预期,这可能是一个性能异常的指示。

如下图所示的第145帧

图8 超长帧Trace信息

每帧的预期耗时(ms) = 1000ms / 帧率。如上图所示,选中超长帧后,可以看到该帧的预期耗时Expected Duration为 8ms 330μs,说明帧率是 120。实际耗时为 92ms 571μs,远超预期耗时,因此被识别为超长帧。超长帧的长时间渲染会直接影响用户体验,导致点击完成时延不达标。

通过上图发现卡顿期间存在较长的ExecuteJS调用,需要查看具体的调用栈。观察ArkTS Callstack泳道无异常后,接下来查看Callstack泳道的函数栈。

关于首帧渲染的特别说明:页面跳转后,由于需要重新加载和渲染新的UI元素,首帧渲染时间往往较长,可能无法达到目标帧率下的预期耗时。因此,性能分析中首帧出现红色(即超出合理预期时间)是较为常见现象,不一定表示存在严重性能问题。但仍需关注首帧渲染时间,必要时进行优化。

Callstack泳道分析Native侧耗时函数

Callstack泳道,该泳道显示Native函数调用泳道,也可以看到Native函数调用栈以及各函数的耗时情况,重点关注主线程和有内容的WorkerThread子泳道。

下图展示了超长帧案例中Callstack的主线程子泳道。

图9 Callstack主线程泳道图

滑动查看右侧权重最高的函数调用栈,定位到MainPage.ets文件第203行代码为主要耗时原因。

ArkUI Component泳道分析组件绘制耗时

ArkUI Component泳道记录了自定义组件以及系统组件的绘制次数、耗时等信息,重点关注相对于其他组件耗时比较久的组件。

图10 ArkUI Component泳道泳道图

然后可以在详情(Details)中使用下图中被框选的按钮过滤目标组件,查看组件在刷新过程中不同阶段的耗时情况。结合函数调用栈和ArkUI Inspector工具,定位目标组件绘制耗时过长的具体原因。

图11 ArkUI Component泳道图Details信息

H:Animator泳道分析动画时长

在页面切换过程中,如果存在加载的 loading 动画,出于用户体验考虑,可将动画停止与网络请求的完成相关联。例如,展示“加载中”状态,直到数据加载完成。通过 H:Animator 泳道,可以观察到动画的耗时。

图12 H:Animator 泳道图

常见问题根因分析

网络请求耗时

在附带网络请求的页面跳转场景中,完成时延较长的绝大多数原因在于网络数据的HTTP请求时间较长。由于网络请求从操作系统侧发起和控制,并且网络环境存在不可控性,因此很难在业务逻辑代码中优化请求速度。因此,提前发起请求就显得尤为重要。通常可以从以下两个方面进行优化:

避免在异步函数中发起网络请求

由于ArkTS单线程EventLoop特性,异步调用的执行时机会被延迟到同步逻辑之后。如果将Http请求接口放在异步函数中,网络请求可能会被UI绘制阻塞,等待第一帧UI绘制结束才开始。如果页面首帧较复杂,这会导致网络请求的延迟时间显著增加。

避免在页面子组件中发起网络请求

由于ArkUI组件的创建基于组件树结构,存在先后顺序。如果在页面的某一子组件中发起网络请求,该请求需要等待前面的组件创建完成。如果前面的组件创建耗时较长,会导致该请求被严重阻塞。

如下图情况,应用页面结构分为Header和Tabs两部分,如果将Tabs内容数据的Http请求放在Tabs组件中发起,由于Tabs组件在UI结构上依赖Header部分,则需要先创建Header,同时又因为Header内容的渲染也依赖网络请求,所以最终导致Tabs的数据请求严重延后。

动画时延耗时

页面转场动画对提升用户体验至关重要。动画时延过长会显著影响用户的点击完成时延。动画完成时间直接关系到用户何时可以开始与应用交互。动画时延的主要原因是动画时长设置过长。

常见的页面转场动画时长参数有:

  1. Tabs组件设置TabContent切换动画时长,即animationDuration属性。
  2. Swiper组件设置子组件切换动画时长,即duration属性。
  3. 页面间转场(pageTransition)设置转场动画时长,即PageTransitionOptions对象中的duration字段。

使用Tabs组件进行页面切换时,当不设置BottomTabBarStyle时默认animationDuration属性有300ms的动画时长,当该属性值设置过长时会导致完成时延变大。接下来将该属性值分别设置为100ms与1000ms来探究animationDuration属性对完成时延的影响。

实验一:设置animationDuration为100ms


@Entry
@Component
struct TabsPositiveExample {
@State currentIndex: number = 0;
private controller: TabsController = new TabsController();
private list: string[] = ['green', 'blue', 'yellow', 'pink'];


@Builder
customContent(color: Color) {
Column()
.width('100%')
.height('100%')
.backgroundColor(color)
}


build() {
Column() {
Row({ space: 10 }) {
ForEach(this.list, (item: string, index: number) => {
Text(item)
.textAlign(TextAlign.Center)
.fontSize(16)
.height(32)
.layoutWeight(1)
.fontColor(this.currentIndex === index ? Color.White : Color.Black)
.backgroundColor(this.currentIndex === index ? Color.Blue : '#f2f2f2')
.borderRadius(16)
.onClick(() => {
this.currentIndex = index;
this.controller.changeIndex(index);
})
}, (item: string, index: number) => JSON.stringify(item) + index)
}
.margin(10)


Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
TabContent() {
this.customContent(Color.Green)
}


TabContent() {
this.customContent(Color.Blue)
}


TabContent() {
this.customContent(Color.Yellow)
}


TabContent() {
this.customContent(Color.Pink)
}
}
.animationDuration(100)
.layoutWeight(1)
.barHeight(0)
.scrollable(false)
}
.width('100%')
}
}

page1.ets

实验二:设置animationDuration为1000ms


@Entry
@Component
struct TabsNegativeExample {
// ...
private controller: TabsController = new TabsController();


// ...


build() {
Column() {
// ...


Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
// ...


}
.barHeight(0)
.layoutWeight(1)
.animationDuration(1000)
.scrollable(false)
}
.width('100%')
}
}

page2.ets

表1 运行效果图

设置animationDuration为100ms

设置animationDuration为1000ms

表2 animationDuration属性值对比

animationDuration属性值

完成时延

100ms

99ms39μs

1000ms

1s7ms693μs

上述示例通过减少animationDuration属性的数值,可以减小Tabs组件切换动画的完成时延。不设置BottomTabBarStyle样式时,动画时长默认为300ms。开发者可根据实际业务场景适当降低动画时长,以提高应用性能。

UI组件优化

转场新页面的组件过于复杂、布局不合理以及资源全量加载等会影响页面首次加载时延,可以采取如下方法进行性能优化:

  • UI优化:可以通过减少嵌套层级、减少渲染时间、使用缓存动效、LazyForEach懒加载、动态import等方式进行优化。相关原理介绍以及场景案例,请参考《点击响应时延分析-UI优化》
  • 全局自定义组件复用:使用自定义组件复用池,实现跨页面的组件复用,实现思路以及场景案例,请参考组件复用
  • 预创建组件:使用组件预创建,可以利用动画执行过程空闲时间进行组件预创建和属性设置。相关原理介绍以及场景案例,请参考《声明式UI中实现组件动态创建》

7 总结

点击完成时延优化是一个系统工程,需要从多个层面综合考虑:

关键优化策略

  1. 任务并行化:尽可能并行执行独立任务

  2. 优先级调度:根据任务重要性合理安排执行顺序

  3. 智能预加载:基于用户行为预测提前加载资源

  4. 多级缓存:合理利用内存和持久化缓存

  5. 资源优化:压缩、懒加载、按需加载资源

华为开发者学堂

Logo

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

更多推荐