简介
开发者实现在应用中跳转显示网页需要分为两个方面:使用@ohos.web.webview提供Web控制能力;使用Web组件提供网页显示的能力。在实际应用中往往由于各种原因导致首次跳转Web网页或Web组件内跳转时出现白屏、卡顿等情况。本文介绍提升Web首页加载与Web网页间跳转速度的几种方法。
优化思路
用户在使用Web组件显示网页时往往会经历四个阶段:无反馈-->白屏-->网页渲染-->完全展示,系统会在各个阶段内分别进行WebView初始化、建立网络连接、接受数据与渲染页面等操作,如图一所示是WebView的启动阶段。
图一 Web组件显示页面的阶段
要优化Web组件的首页加载性能,可以从图例标记的三个阶段来进行优化:
- 在WebView的初始化阶段:应用打开WebView的第一步是启动浏览器内核,而这段时间由于WebView还不存在,所有后续的步骤是完全阻塞的。因此可以考虑在应用中预先完成初始化WebView,以及在初始化的同时通过预先加载组件内核、完成网络请求等方法,使得WebView初始化不是完全的阻塞后续步骤,从而减小耗时。
- 在建立连接阶段:当开发者提前知道访问的网页地址,我们可以预先建立连接,进行DNS预解析。
- 在接收资源数据阶段:当开发者预先知道用户下一页会点击什么页面的时候,可以合理使用缓存和预加载,将该页面的资源提前下载到缓存中。
综上所述,开发者可以通过方法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建链操作,并不获取主资源子资源。
参数:
参数名 | 类型 | 说明 |
url | string | 预连接的url。 |
preconnectable | boolean | 是否进行预连接。如果preconnectable为true,则对url进行dns解析,socket建链预连接;如果preconnectable为false,则不做任何预连接操作。 |
numSockets | number | 要预连接的socket数。socket数目连接需要大于0,最多允许6个连接。 |
使用方法如下:
// 开启预连接需要先使用上述方法预加载WebView内核。
webview.WebviewController.initializeWebEngine();
// 启动预连接,连接地址为即将打开的网址。
webview.WebviewController.prepareForPageLoad("https://www.example.com", true, 2);
预加载下一页
开发者可以在onPageEnd阶段进行预加载,当真正去加载下一个页面的时候,如果预加载已经成功,则相当于直接从缓存中加载页面资源,速度更快。一般来说能够准确预测到用户下一步要访问的页面的时候,可以进行预加载将要访问的页面,比如小说下一页, 浏览器在地址栏输入过程中识别到用户将要访问的页面等。
@ohos.web.webview提供prefetchPage方法实现在预测到将要加载的页面之前调用,提前下载页面所需的资源,包括主资源子资源,但不会执行网页JavaScript代码或呈现网页,以加快加载速度。
参数:
参数名 | 类型 | 说明 |
url | string | 预加载的url。 |
additionalHeaders | Array | url的附加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个。
实践案例
- 创建载体,并创建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;
}
});
}
2.创建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);
}
3.通过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左右的跳转时间。
最后
如果大家觉得这篇内容对学习鸿蒙开发有帮助,我想邀请大家帮我三个小忙:
点赞,转发,有大家的 『点赞和评论』,才是我创造的动力。
关注小编,同时可以期待后续文章ing