鸿蒙(HarmonyOS)性能优化实战-Web组件开发性能提升方案

简介

开发者实现在应用中跳转显示网页需要分为两个方面:使用@ohos.web.webview提供Web控制能力;使用Web组件提供网页显示的能力。在实际应用中往往由于各种原因导致首次跳转Web网页或Web组件内跳转时出现白屏、卡顿等情况。本文介绍提升Web首页加载与Web网页间跳转速度的几种方法。

优化思路

用户在使用Web组件显示网页时往往会经历四个阶段:无反馈–>白屏–>网页渲染–>完全展示,系统会在各个阶段内分别进行WebView初始化、建立网络连接、接受数据与渲染页面等操作,如图一所示是WebView的启动阶段。

图一 Web组件显示页面的阶段

要优化Web组件的首页加载性能,可以从图例标记的三个阶段来进行优化:

  1. 在WebView的初始化阶段:应用打开WebView的第一步是启动浏览器内核,而这段时间由于WebView还不存在,所有后续的步骤是完全阻塞的。因此可以考虑在应用中预先完成初始化WebView,以及在初始化的同时通过预先加载组件内核、完成网络请求等方法,使得WebView初始化不是完全的阻塞后续步骤,从而减小耗时。
  2. 在建立连接阶段:当开发者提前知道访问的网页地址,我们可以预先建立连接,进行DNS预解析。
  3. 在接收资源数据阶段:当开发者预先知道用户下一页会点击什么页面的时候,可以合理使用缓存和预加载,将该页面的资源提前下载到缓存中。

综上所述,开发者可以通过方法1和2来提升Web首页加载速度,在应用创建Ability的时候,在OnCreate阶段预先初始化内核。随后在onAppear阶段进行预解析DNS、预连接要加载的首页。
在网页跳转的场景,开发者也可以通过方法3,在onPageEnd阶段预加载下一个要访问的页面,提升Web网页间的跳转和显示速度,如图二所示。

图二 Web组件的生命周期回调函数

优化方法

提前初始化内核

当应用首次打开时,默认不会初始化浏览器内核,只有当创建WebView实例的时候,才会开始初始化浏览器内核。
为了能提前初始化WebView实例,@ohos.web.webview提供了initializeWebEngine方法。该方法实现在Web组件初始化之前,通过接口加载Web引擎的动态库文件,从而提前进行Web组件动态库的加载和Web内核主进程的初始化,最终以提高启动性能,减少白屏时间。
使用方法如下:

// ../src/main/ets/pages/WebInitialized.ets

import webview from '@ohos.web.webview';

...
  aboutToAppear() {
    // 通过WebviewController可以控制Web组件各种行为。一个WebviewController对象只能控制一个Web组件,且必须在Web组件和WebviewController绑定后,才能调用WebviewController上的方法(静态方法除外)。
    webview.WebviewController.initializeWebEngine();
}

预解析DNS、预连接

WebView在onAppear阶段进行预连接socket, 当Web内核真正发起请求的时候会直接复用预连接的socket,如果当前预解析还没完成,真正发起网络请求进行DNS解析的时候也会复用当前正在执行的DNS解析任务。同理即使预连接的socket还没有连接成功,Web内核也会复用当前正在连接中的socket,进而优化资源的加载过程。
@ohos.web.webview提供了prepareForPageLoad方法实现预连接url,在加载url之前调用此API,对url只进行DNS解析、socket建链操作,并不获取主资源子资源。
参数:

参数名类型说明
urlstring预连接的url。
preconnectableboolean是否进行预连接。如果preconnectable为true,则对url进行dns解析,socket建链预连接;如果preconnectable为false,则不做任何预连接操作。
numSocketsnumber要预连接的socket数。socket数目连接需要大于0,最多允许6个连接。

使用方法如下:

// 开启预连接需要先使用上述方法预加载WebView内核。
webview.WebviewController.initializeWebEngine();
// 启动预连接,连接地址为即将打开的网址。
webview.WebviewController.prepareForPageLoad("https://www.example.com", true, 2);

预加载下一页

开发者可以在onPageEnd阶段进行预加载,当真正去加载下一个页面的时候,如果预加载已经成功,则相当于直接从缓存中加载页面资源,速度更快。一般来说能够准确预测到用户下一步要访问的页面的时候,可以进行预加载将要访问的页面,比如小说下一页, 浏览器在地址栏输入过程中识别到用户将要访问的页面等。
@ohos.web.webview提供prefetchPage方法实现在预测到将要加载的页面之前调用,提前下载页面所需的资源,包括主资源子资源,但不会执行网页JavaScript代码或呈现网页,以加快加载速度。
参数:

参数名类型说明
urlstring预加载的url。
additionalHeadersArrayurl的附加HTTP请求头。

使用方法如下:

// ../src/main/ets/pages/WebBrowser.ets

import webview from '@ohos.web.webview';
...

  controller: webview.WebviewController = new webview.WebviewController();
    ...
    Web({ src: 'https://www.example.com', controller: this.controller })
      .onPageEnd((event) => {
         ...
         // 在确定即将跳转的页面时开启预加载
         this.controller.prefetchPage('https://www.example.com/nextpage');
      })
    Button('下一页')
      .onClick(() => {
         ...
         // 跳转下一页
         this.controller.loadUrl('https://www.example.com/nextpage');
      })

预渲染优化

原理介绍

预渲染优化适用于Web页面启动和跳转场景,例如,进入首页后,跳转到其他子页。与预连接、预下载不同的是,预渲染需要开发者额外创建一个新的ArkWeb组件,并在后台对其进行预渲染,此时该组件并不会立刻挂载到组件树上,即不会对用户呈现(组件状态为Hidden和InActive),开发者可以在后续使用中按需动态挂载。

具体原理如下图所示,首先需要定义一个自定义组件封装ArkWeb组件,该ArkWeb组件被离线创建,被包含在一个无状态的节点NodeContainer中,并与相应的NodeController绑定。该ArkWeb组件在后台完成预渲染后,在需要展示该ArkWeb组件时,再通过NodeController将其挂载到ViewTree的NodeContainer中,即通过NodeController绑定到对应的NodeContainer组件。预渲染通用实现的步骤如下:

创建自定义ArkWeb组件:开发者需要根据实际场景创建封装一个自定义的ArkWeb组件,该ArkWeb组件被离线创建。
创建并绑定NodeController:实现NodeController接口,用于自定义节点的创建、显示、更新等操作的管理。并将对应的NodeController对象放入到容器中,等待调用。
绑定NodeContainer组件:将NodeContainer与NodeController进行绑定,实现动态组件页面显示。

图三 预渲染优化原理图

说明
预渲染相比于预下载、预连接方案,会消耗更多的内存、算力,仅建议针对高频页面使用,单应用后台创建的ArkWeb组件要求小于200个。

实践案例
  1. 创建载体,并创建ArkWeb组件
    // 载体Ability
    // EntryAbility.ets
    import {createNWeb} from "../pages/common"
    onWindowStageCreate(windowStage: window.WindowStage): void {
      windowStage.loadContent('pages/Index', (err, data) => {
        // 创建ArkWeb动态组件(需传入UIContext),loadContent之后的任意时机均可创建
        createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext());
        if (err.code) {
          return;
        }
      });
    }
  1. 创建NodeContainer和对应的NodeController,渲染后台ArkWeb组件
    // 创建NodeController
    // common.ets
    import { UIContext } from '@kit.ArkUI';
    import { webview } from '@kit.ArkWeb';
    import { NodeController, BuilderNode, Size, FrameNode }  from '@kit.ArkUI';
    // @Builder中为动态组件的具体组件内容
    // Data为入参封装类
    // 调用onActive,开启渲染
    @Builder
    function WebBuilder(data:Data) {
      Column() {
        Web({ src: data.url, controller: data.controller })
          .onPageBegin(() => {
            data.controller.onActive();
          })
          .width("100%")
          .height("100%")
      }
    }
    let wrap = wrapBuilder<Data[]>(WebBuilder);
    // 用于控制和反馈对应的NodeContianer上的节点的行为,需要与NodeContainer一起使用
    export class myNodeController extends NodeController {
      private rootnode: BuilderNode<Data[]> | null = null;
      // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContianer中
      // 在对应NodeContianer创建的时候调用、或者通过rebuild方法调用刷新
      makeNode(uiContext: UIContext): FrameNode | null {
        console.log(" uicontext is undifined : "+ (uiContext === undefined));
        if (this.rootnode != null) {
          // 返回FrameNode节点
          return this.rootnode.getFrameNode();
        }
        // 返回null控制动态组件脱离绑定节点
        return null;
      }
      // 当布局大小发生变化时进行回调
      aboutToResize(size: Size) {
        console.log("aboutToResize width : " + size.width  +  " height : " + size.height )
      }
      // 当controller对应的NodeContainer在Appear的时候进行回调
      aboutToAppear() {
        console.log("aboutToAppear")
      }
      // 当controller对应的NodeContainer在Disappear的时候进行回调
      aboutToDisappear() {
        console.log("aboutToDisappear")
      }
      // 此函数为自定义函数,可作为初始化函数使用
      // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容
      initWeb(url:string, uiContext:UIContext, control:WebviewController) {
        if(this.rootnode != null)
        {
          return;
        }
        // 创建节点,需要uiContext
        this.rootnode = new BuilderNode(uiContext)
        // 创建动态Web组件
        this.rootnode.build(wrap, { url:url, controller:control })
      }
    }
    // 创建Map保存所需要的NodeController
    let NodeMap:Map<string, myNodeController | undefined> = new Map();
    // 创建Map保存所需要的WebViewController
    let controllerMap:Map<string, WebviewController | undefined> = new Map();
    // 初始化需要UIContext 需在Ability获取
    export const createNWeb = (url: string, uiContext: UIContext) => {
      // 创建NodeController
      let baseNode = new myNodeController();
      let controller = new webview.WebviewController() ;
      // 初始化自定义web组件
      baseNode.initWeb(url, uiContext, controller);
      controllerMap.set(url, controller)
      NodeMap.set(url, baseNode);
    }
    // 自定义获取NodeController接口
    export const getNWeb = (url : string) : myNodeController | undefined => {
      return NodeMap.get(url);
    }
  1. 通过NodeContainer使用已经预渲染的页面
    // 使用NodeController的Page页
    // Index.ets
    import {createNWeb, getNWeb} from "./common"
    @Entry
    @Component
    struct Index {
      build() {
        Row() {
          Column() {
            // NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode
            // Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示
            NodeContainer(getNWeb("https://www.example.com"))
              .height("90%")
              .width("100%")
          }
          .width('100%')
        }
        .height('100%')
      }
    }

性能分析

场景示例

构建通过点击按钮跳转Web网页和在网页内跳转页面的场景,在点击按钮触发跳转事件、Web组件触发OnPageEnd事件处使用Hilog打点记录时间戳。

反例

入口页通过router实现跳转

// ../src/main/ets/pages/WebUninitialized.ets

...
Button('进入网页')
  .onClick(() => {
    hilog.info(0x0001, "WebPerformance", "UnInitializedWeb");
    router.pushUrl({ url: 'pages/WebBrowser' });
  })

Web页使用Web组件加载指定网页

// ../src/main/ets/pages/WebBrowser.ets

...
Web({ src: 'https://www.example.com', controller: this.controller })
  .domStorageAccess(true)
  .onPageEnd((event) => {
     if (event) {
       hilog.info(0x0001, "WebPerformance", "WebPageOpenEnd");
     }
  })
正例

入口页提前进行Web组件的初始化和预连接

// ../src/main/ets/pages/WebInitialized.ets

import webview from '@ohos.web.webview';

...
Button('进入网页')
  .onClick(() => {
     hilog.info(0x0001, "WebPerformance", "InitializedWeb");
     router.pushUrl({ url: 'pages/WebBrowser' });
  })
...
aboutToAppear() {
  webview.WebviewController.initializeWebEngine();
  webview.WebviewController.prepareForPageLoad("https://www.example.com", true, 2);
}

Web页加载的同时使用prefetchPage预加载下一页

// ../src/main/ets/pages/WebBrowser.ets

import webview from '@ohos.web.webview';

...
  controller: webview.WebviewController = new webview.WebviewController();
    ...
    Web({ src: 'https://www.example.com', controller: this.controller })
      .domStorageAccess(true)
      .onPageEnd((event) => {
         if (event) {
           hilog.info(0x0001, "WebPerformance", "WebPageOpenEnd");
           this.controller.prefetchPage('https://www.example.com/nextpage');
         }
      })

数据对比

通过分别抓取正反示例的trace数据后使用SmartPerf Host工具分析可以得出以下结论:

从点击按钮进入Web首页到Web组件触发OnPageEnd事件,表示首页加载完成。对比优化前后时延可以得出,使用提前初始化内核和预解析、预连接可以减少平均100ms左右的加载时间。

从Web首页内点击跳转下一页按钮到Web组件触发OnPageEnd事件,表示页面间跳转完成。对比优化前后时延可以得出,使用预加载下一页方法可以减少平均40~50ms左右的跳转时间。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值