鸿蒙原生APP性能优化之Web场景性能优化(一)

往期推文全新看点

概述

ArkWeb(方舟Web)是一个Web组件平台,旨在为应用程序提供展示Web页面内容的功能,并为开发者提供丰富的能力,包括页面加载、页面交互、页面调试等功能。在这个数字化时代,页面显示的速度直接体现了应用的流畅性,影响着用户对应用的印象和体验。快速加载并展示页面不仅可以吸引用户留在应用上,还能减少他们的等待时间和不耐烦情绪,从而提升用户的满意度。

Web页面的显示过程可以被分为多个阶段,包括DNS解析、建立连接、发送请求、接收响应、解析HTML、下载资源等步骤。在这个过程中,许多因素都会对页面显示速度产生影响,比如网络延迟、服务器响应时间、页面大小、资源压缩等。为了提升Web页面显示速度,开发者可以在Web页面加载、资源下载和页面渲染等方面进行优化,以提升性能和用户体验。

本文将介绍以下常见的优化方式。

  • Web页面加载优化:Web页面加载速度对于用户体验至关重要,提高页面加载的速度可以直接提升应用的流畅性。
  • JSBridge:通过JSBridge通信,可以解决ArkTS环境的冗余切换,避免造成UI阻塞。
  • 同层渲染:通过将页面元素分层渲染,可以减少页面重绘和重排的次数,提高页面渲染效率。

ArkWeb(方舟Web)为开发者提供了优化页面显示速度的方法。通过采取这些优化方式,开发者可以改善应用程序的性能和用户体验,使用户能够更快速、更流畅地浏览Web页面,从而提升用户满意度和留存率。

Web页面加载性能优化指导

Web页面加载流程

Web页面加载流程包括网络连接、资源下载(包括等待网络资源下载)、DOM解析、JavaScript代码编译执行、渲染等。页面加载中,比较关键的节点有网络连接、资源下载和完整的页面渲染,本文将主要对以下关键节点的耗时进行优化。

  • 预启动Web渲染进程:预启动Web渲染进程指用户可以在业务需要的Web页面启动前,加载一个空白的Web组件,在至少一个Web组件存活时,Web渲染进程进程会一直存在,节省了用户后续启动Web组件拉起渲染进程的时间,加快页面加载速度。

  • 预解析:预解析指预先对DNS进行解析,可以节省DNS解析的时间,从而优化Web的加载耗时。

  • 预连接:预连接包含预解析的步骤,可以在用户请求页面之前提前进行DNS解析和socket连接建立,这样当用户真正请求页面时,服务器和浏览器之间已经建立好了连接,可以直接传输数据,减少了网络延迟,提升了页面加载速度。

  • 预下载:预下载指在页面加载之前提前下载所需的资源,以避免在页面加载过程中资源下载导致的阻塞和耗时。通过预下载,可以在浏览器加载页面时,提前获取到所需的资源如图片、CSS文件、JavaScript文件等。通过提前下载这些资源,可以避免在页面加载时因为资源未加载完成而导致页面渲染延迟的情况发生。通过合理地使用预下载技术,用户在访问页面时可以更快地看到页面内容,提高整体性能,提升用户体验。

  • 预渲染:预渲染指在后台对需要加载的页面进行预先渲染,提前完成整个页面加载的流程。当用户需要访问该页面时,可以直接切换至前台展示,实现页面“秒开”的效果。预渲染要求在进行DOM解析、JavaScript执行和页面渲染之前,已经完成了所需资源的下载工作,否则可能会导致页面内容不完整或者渲染错误的情况。通过预渲染,可以显著减少用户等待页面加载的时间,特别是对于一些需要加载大量资源或者有复杂交互的页面。

  • 预取POST:预取POST指当即将加载的Web页面中存在POST请求且POST请求耗时较长时,可对POST请求进行预获取,消除等待POST请求数据下载完成的耗时,当用户真正发起POST请求时,进行拦截替换,加快页面加载速度,提高用户体验。

  • 预编译JavaScript生成字节码缓存(Code Cache):该方案会将使用到的JavaScript文件编译成字节码并缓存到本地,在页面首次加载时节省编译时间。

  • 资源拦截替换的JavaScript生成字节码缓存(Code Cache):该方案会将资源拦截替换场景下的JavaScript文件编译成字节码并缓存到本地,节省在页面非首次加载时的编译时间。

  • 离线资源免拦截注入:在页面加载之前,离线资源免拦截注入会将需要使用的图片、样式表和脚本资源注入到内存缓存中,节省页面首次加载时的网络请求时间。

  • 资源拦截替换加速:在原本的资源拦截替换接口基础上,资源拦截替换加速支持了ArrayBuffer格式的入参,开发者无需在应用侧进行ArrayBuffer到String格式的转换,可直接使用ArrayBuffer格式的数据进行拦截替换。

图1 Web页面加载流程

**由于所有的关键点都是建立在预处理的思路上,因此如果用户实际并未打开预处理的Web页面,将会造成额外的资源消耗。**各优化方法具体的效果、代价和适用场景的对比如下表所示。

表1 优化方法对比表格

优化方法 效果(优化数据仅供参考) 适配难度 影响 适用场景
预启动Web渲染进程 消除拉起Web渲染进程的耗时,约140ms。 额外的内存、算力。 高概率被使用的Web页面。
预解析 消除用户真正启动的Web网页域名解析的耗时,约66ms。 可能存在提前解析了用户未启动的Web网页域名。 中高概率被使用的Web页面。
预连接 消除用户真正启动的Web网页域名解析、网络连接耗时,约80ms。 可能存在提前连接了用户未启动Web网页资源。 中高概率被使用的Web页面。
预下载 消除网络GET请求下载带来的耗时及阻塞DOM解析、JavaScript执行的耗时,约641ms。 额外的网络连接、下载、存储资源。 高概率被使用的Web页面。
预渲染 能实现页面“秒开”效果,将页面加载时延降到最低,约486ms。 额外的网络连接、下载、存储和渲染消耗。 超高概率被使用的Web页面。
预取POST 消除网络POST请求下载带来的耗时及阻塞DOM解析、JavaScript执行的耗时,约313ms。 额外的网络连接、下载、存储资源。 高概率被使用的Web页面。
预编译JavaScript生成字节码缓存 消除JavaScript编译的耗时,优化数据根据JS资源大小而定,5.76Mb资源预编译时约有2915ms收益。 额外的存储资源。 加载HTTP/HTTPS协议JavaScript的Web页面,在第一及第二次优化加载性能。
资源拦截替换的JavaScript生成字节码缓存 消除JavaScript编译的耗时,优化数据根据JS资源大小而定,2.4Mb资源拦截替换时约有67ms收益。 额外的存储资源。 加载自定义协议JavaScript的Web页面,在第三次及之后的时机优化加载性能。
离线资源免拦截注入 消除资源加载到内存的耗时,优化数据根据资源大小而定,25Mb资源注入时约有1240ms收益。 额外的存储资源。 高概率被使用的资源。
资源拦截替换加速 节省了转换时间,同时对ArrayBuffer格式的数据传输方式进行了优化,优化数据根据资源大小而定,10Kb资源拦截替换时约有20ms收益。 - ArrayBuffer格式的数据传输。

预启动Web渲染进程

原理介绍

预启动Web渲染进程方案适用于Web页面启动场景,本方案需要开发者额外创建一个新的ArkWeb组件,此时该组件并不会对用户显示,但是会提前拉起渲染进程,且Web渲染进程在Web组件全部销毁前会一直存在,并于应用侧全局共用,节省了后续Web组件加载时用于启动Web渲染进程的耗时。

建议开发者在Web页面启动前执行预启动Web渲染进程方案,如应用冷启动阶段或者广告阶段,如果无法在冷启动期间进行Web渲染进程预启动,建议在空闲时间预启动Web渲染进程。

图2预启动Web渲染流程

说明

  1. 该方案通过创建一个空白的ArkWeb组件来预启动Web渲染进程,额外创建ArkWeb组件会消耗内存、算力,预创建一个空白的Web组件约消耗200Mb内存,所以建议后续页面加载建议复用预创建的Web组件。
  2. 应用侧全局共用一个Web渲染进程,仅在Web组件全部销毁时Web渲染进程才会被杀掉,因此建议应用至少保证一个Web组件存活。

实践案例

【不推荐用法】

点击跳转下一页,直接加载Web页面

说明
案例中涉及网络地址访问,需配置网络权限。

// Index.ets
@Entry
@Component
struct Index {
  pageInfos: NavPathStack = new NavPathStack()
  build() {
    Navigation(this.pageInfos) {
      Column() {
        Button('加载测试页面', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            // 将name指定的NavDestination页面信息入栈
            this.pageInfos.pushPath({ name: 'pageOne' })
          })
      }
    }.title('NavIndex')
  }
}

// Second.ets
import { webview } from '@kit.ArkWeb'

@Builder
export function PageOneBuilder() {
  Second()
}

@Component
export struct Second {
  webviewController: webview.WebviewController = new webview.WebviewController();
  aboutToAppear(): void {
    // 输出Web页面开始加载时间
    console.info(`load page start time: ${Date.now()}`);
  }
  build() {
    NavDestination() {
      Row() {
        Column() {
          // url请替换为真实地址
          Web({ src: 'https://www.example.com', controller: new webview.WebviewController() })
            .height('100%')
            .width('100%')
            .onPageEnd((event) => {
              // 输出Web页面加载完成时间
              console.info(`load page end time: ${Date.now()}`);
            })
        }
        .width('100%')
      }
      .height('100%')
    }
  }
}

点击“加载测试页面”按钮,页面加载完成耗时如下图所示:

【推荐用法】

在后台创建一个ArkWeb组件来预先启动用于渲染的Web渲染进程。

  1. 创建Node和对应的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为入参封装类
class Data {
  url: string = 'https://www.example.com';
  controller: WebviewController = new webview.WebviewController();
}

@Builder
function webBuilder(data: Data) {
  Column() {
    Web({ src: data.url, controller: data.controller })
      .domStorageAccess(true)
      .zoomAccess(true)
      .fileAccess(true)
      .mixedMode(MixedMode.All)
      .width('100%')
      .height('100%')
      .onPageEnd((event) => {
        // 输出Web页面加载完成时间
        console.info(`load page end time: ${Date.now()}`);
      })
  }
}

let wrap = wrapBuilder<Data[]>(webBuilder);

// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用
export class MyNodeController extends NodeController {
  private rootnode: BuilderNode<Data[]> | null = null;
  private root: FrameNode | null = null;
  private rootWebviewController: webview.WebviewController | null = null;

  // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContainer中
  // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新
  makeNode(uiContext: UIContext): FrameNode | null {
    console.info(' uicontext is undifined : ' + (uiContext === undefined));
    if (this.rootnode != null) {
      const parent = this.rootnode.getFrameNode()?.getParent();
      if (parent) {
        console.info(JSON.stringify(parent.getInspectorInfo()));
        parent.removeChild(this.rootnode.getFrameNode());
        this.root = null;
      }
      this.root = new FrameNode(uiContext);
      this.root.appendChild(this.rootnode.getFrameNode());
      // 返回FrameNode节点
      return this.root;
    }
    // 返回null控制动态组件脱离绑定节点
    return null;
  }

  // 当布局大小发生变化时进行回调
  aboutToResize(size: Size) {
    console.info('aboutToResize width : ' + size.width + ' height : ' + size.height);
  }

  // 当controller对应的NodeContainer在Appear的时候进行回调
  aboutToAppear() {
    console.info('aboutToAppear');
  }

  // 当controller对应的NodeContainer在Disappear的时候进行回调
  aboutToDisappear() {
    console.info('aboutToDisappear');
  }

  // 此函数为自定义函数,可作为初始化函数使用
  // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容
  initWeb(url: string, uiContext: UIContext, control: WebviewController) {
    if (this.rootnode != null) {
      return;
    }
    // 绑定预创建的WebviewController
    this.rootWebviewController = control;
    // 创建节点,需要uiContext
    this.rootnode = new BuilderNode(uiContext);
    // 创建动态Web组件
    this.rootnode.build(wrap, { url: url, controller: control });
  }

  // 此函数为自定义函数,可作为初始化函数使用
  loadUrl(url: string) {
    if (this.rootWebviewController !== null) {
      // 复用预创建组件,重新加载url
      this.rootWebviewController.loadUrl(url);
    }
  }
}

// 创建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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值