鸿蒙 Web组件预加载、复用
鸿蒙系统的Web组件支持离线预加载功能,通过NodeContainer和NodeController实现动态挂载机制。开发者可预先创建Web组件但不立即挂载,在需要时快速显示。该技术包含预启动渲染进程和预渲染页面两种优化场景:前者通过加载空白页提前启动渲染进程;后者后台完成渲染后暂停以避免功耗问题。系统提供组件复用机制,支持加载空白页后重新利用,并可通过回收接口释放资源。注意事项包括内存占用(每个
本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新
鸿蒙系统中,Web组件能够实现在不同窗口的组件树上进行挂载或移除操作。这一能力使得开发者可以预先创建Web组件,从而实现性能优化。例如,当Tab页为Web组件时,可以在后台预先渲染,便于即时显示。
离线Web组件基于自定义占位组件[NodeContainer]实现,核心原理是:构建支持命令式创建的Web组件,此类组件创建后不会立即挂载到组件树中,状态为Hidden和Inactive,因此不会立即对用户呈现。可以在后续使用中按需动态挂载这些组件。
二、离线Web组件
1. 预启动渲染进程
-
在未进入Web页面时,提前创建空Web组件
-
启动Web的渲染进程,为后续使用做好准备
-
节省Web组件加载时启动渲染进程的时间
2. 预渲染Web页面
-
在Web页面启动或跳转场景下使用
-
预先在后台创建Web组件,加载数据并完成渲染
-
实现Web页面启动或跳转时的快速显示
三、工作原理
架构流程图
离屏创建Web组件
↓
定义自定义组件封装Web组件
↓
封装于无状态的NodeContainer节点中
↓
与NodeController组件绑定
↓
Web组件后台预渲染
↓
需要展示时通过NodeController挂载
↓
挂载到ViewTree的NodeContainer中
↓
显示
组件关系
-
NodeContainer:自定义占位组件,用于挂载动态组件
-
NodeController:节点控制器,控制和反馈NodeContainer上的节点行为
-
BuilderNode:构建动态组件的核心类
-
WebviewController:Web组件控制器,管理Web行为
四、创建离线Web组件
1. 在Ability中预创建Web组件
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err, data) => {
// 创建Web动态组件(需传入UIContext)
// loadContent之后的任意时机均可创建
createNWeb('www.example.com',
windowStage.getMainWindowSync().getUIContext());
if (err.code) {
return;
}
});
}
2. 创建NodeController和Builder
// Common.ets
import { UIContext, NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI';
import { webview } from '@kit.ArkWeb';
// Data为入参封装类
class Data {
public url: ResourceStr = 'www.example.com';
public controller: WebviewController = new webview.WebviewController();
}
@Builder
function webBuilder(data: Data) {
Column() {
Web({ src: data.url, controller: data.controller })
.width('100%')
.height('100%')
}
}
let wrap = wrapBuilder<Data[]>(webBuilder);
// 用于控制和反馈对应的NodeContainer上的节点的行为
export class MyNodeController extends NodeController {
private rootNode: BuilderNode<Data[]> | null = null;
// 必须重写的方法:构建节点数、返回节点挂载在对应NodeContainer中
makeNode(uiContext: UIContext): FrameNode | null {
console.info('uicontext is undefined : ' + (uiContext === undefined));
if (this.rootNode !== null) {
return this.rootNode.getFrameNode(); // 返回FrameNode节点
}
return null; // 返回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
initWeb(url: ResourceStr, 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<ResourceStr, MyNodeController | undefined> = new Map();
// 创建Map保存所需要的WebViewController
let controllerMap: Map<ResourceStr, WebviewController | undefined> = new Map();
// 初始化需要UIContext,需在Ability获取
export const createNWeb = (url: ResourceStr, 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: ResourceStr): MyNodeController | undefined => {
return nodeMap.get(url);
}
3. 在页面中挂载显示
// Index.ets
import { getNWeb } from './common'
@Entry
@Component
struct Index {
build() {
Row() {
Column() {
// NodeContainer用于与NodeController节点绑定
// rebuild会触发makeNode
// Page页通过NodeContainer接口绑定NodeController
NodeContainer(getNWeb('www.example.com'))
.height('90%')
.width('100%')
}
.width('100%')
}
.height('100%')
}
}
五、预启动优化
场景
-
采用单渲染进程模式的应用(全局共享一个Web渲染进程)
-
Web渲染进程仅在所有Web组件都被销毁后才会终止
-
建议应用至少保持一个Web组件处于活动状态
原理
在onWindowStageCreate时预创建Web组件加载blank页面,提前启动Render进程,从index跳转到index2时,优化Web渲染进程启动和初始化的耗时。
代码实现
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err, data) => {
// 创建空的Web动态组件,加载about:blank页面
// 提前启动Render进程
createNWeb('about:blank',
windowStage.getMainWindowSync().getUIContext());
if (err.code) {
return;
}
});
}
// index.ets - 首页
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct Index1 {
webviewController: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
// 已经预启动Render进程,跳转后Web页面加载更快
Button('Jump to web page').onClick(()=>{
this.getUIContext().getRouter().pushUrl({url: 'pages/index2'});
})
.width('100%')
.height('100%')
}
}
}
// index2.ets - 目标页
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct index2 {
webviewController: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Row() {
Column() {
// 由于渲染进程已预启动,加载速度更快
Web({src: 'www.example.com', controller: this.webviewController})
.width('100%')
.height('100%')
}
.width('100%')
}
.height('100%')
}
}
注意事项
-
内存开销:创建额外的Web组件会产生额外内存占用(每个Web组件约200MB)
-
适用条件:仅在单渲染进程模式下优化效果显著
-
建议:避免一次性创建大量离线Web组件
六、预渲染Web页面优化
适用场景
-
Web页面启动和跳转场景(如进入首页后跳转到子页)
-
建议在高命中率的页面使用该方案
实现原理
提前创建离线Web组件,设置Web为Active状态来开启渲染引擎,进行后台渲染。预渲染完成后立即停止渲染,防止发热和功耗问题。
代码实现
// Common.ets - 带预渲染控制的NodeController
import { UIContext } from '@kit.ArkUI';
import { webview } from '@kit.ArkWeb';
import { NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI';
// 通过布尔变量shouldInactive控制网页在后台完成预渲染后停止渲染
let shouldInactive: boolean = true;
class Data {
public url: string = 'www.example.com';
public controller: WebviewController = new webview.WebviewController();
}
@Builder
function webBuilder(data: Data) {
Column() {
Web({ src: data.url, controller: data.controller })
.onPageBegin(() => {
// 调用onActive,开启渲染
data.controller.onActive();
})
.onFirstMeaningfulPaint(() => {
// 首次有意义绘制完成时触发
if (!shouldInactive) {
return;
}
// 在预渲染完成时触发,停止渲染,防止发热和功耗问题
data.controller.onInactive();
shouldInactive = false;
})
.width('100%')
.height('100%')
}
}
let wrap = wrapBuilder<Data[]>(webBuilder);
export class MyNodeController extends NodeController {
private rootNode: BuilderNode<Data[]> | null = null;
makeNode(uiContext: UIContext): FrameNode | null {
if (this.rootNode !== null) {
return this.rootNode.getFrameNode();
}
return null;
}
aboutToResize(size: Size) {
console.info('aboutToResize width : ' + size.width + ' height : ' + size.height);
}
aboutToAppear() {
console.info('aboutToAppear');
// 切换到前台后,不需要停止渲染
shouldInactive = false;
}
aboutToDisappear() {
console.info('aboutToDisappear');
}
initWeb(url: string, uiContext: UIContext, control: WebviewController) {
if (this.rootNode !== null) {
return;
}
this.rootNode = new BuilderNode(uiContext);
this.rootNode.build(wrap, { url: url, controller: control });
}
}
// 创建Map保存所需要的NodeController
let nodeMap: Map<string, MyNodeController | undefined> = new Map();
let controllerMap: Map<string, WebviewController | undefined> = new Map();
export const createNWeb = (url: string, uiContext: UIContext) => {
let baseNode = new MyNodeController();
let controller = new webview.WebviewController();
baseNode.initWeb(url, uiContext, controller);
controllerMap.set(url, controller)
nodeMap.set(url, baseNode);
}
export const getNWeb = (url: string): MyNodeController | undefined => {
return nodeMap.get(url);
}
预渲染生命周期
-
onPageBegin:页面开始加载时调用onActive开启渲染
-
onFirstMeaningfulPaint:首次有意义绘制完成时调用onInactive停止渲染
-
aboutToAppear:组件出现在前台时重置控制变量
注意事项
-
需要明确预加载的资源
-
不建议预渲染包含自动播放音视频的页面
-
预渲染完成后应立即停止渲染,防止发热和功耗问题
-
onFirstMeaningfulPaint接口仅适用于http和https网页
七、复用离线Web组件
应用有多个UI页面都需要显示Web内容时,建议复用离线Web组件,减少组件创建和销毁的性能消耗以及创建多个Web组件的内存占用。
方法
步骤1:离线Web组件不再使用时加载空白页
// 组件即将被回收时,加载about:blank
aboutToDisappear() {
// 调用WebController的loadUrl方法加载about:blank空页面
// 为下次其他UI页面复用做准备
this.webviewController?.loadUrl('about:blank');
}
步骤2:新UI页面复用时加载目标页面
// 复用离线Web组件时
onPageShow() {
// 再次调用loadUrl加载需要的Web页面
this.webviewController?.loadUrl('www.new-page.com');
}
复用优势
-
减少Web组件创建和销毁的性能消耗
-
降低多个Web组件同时存在时的内存占用
-
提升页面切换的响应速度
八、释放离线Web组件
释放时机
-
应用退至后台
-
明确在特定时间段内不再需要使用离线Web组件时
重要前提
仅当离线Web组件未绑定到UI页面时,才能释放该组件,否则可能导致NodeContainer组件显示空白。
代码
// Common.ets - 完整版
// 创建Map保存所需要的NodeController
let nodeMap: Map<ResourceStr, MyNodeController | undefined> = new Map();
// 创建保存uiContext的全局变量
let globalUiContext: UIContext | undefined = undefined;
// 创建Set保存已释放的离线组件url信息
let recycledNWebs: Set<ResourceStr> = new Set()
// 初始化需要UIContext,需在Ability获取
export const createNWeb = (url: ResourceStr, uiContext: UIContext) => {
console.info('createNWeb, url = ' + url);
if (!globalUiContext) {
globalUiContext = uiContext;
}
if (getNWeb(url)) {
console.info('createNWeb, already exit this node, url:' + url);
return;
}
let baseNode = new MyNodeController();
// 初始化自定义Web组件
baseNode.initWeb(url, uiContext);
nodeMap.set(url, baseNode);
recycledNWebs.delete(url); // 从已释放集合中移除
}
// 自定义释放/回收离线Web组件的接口
// 释放成功返回true
export const recycleNWeb = (url: ResourceStr, force: boolean = false): boolean => {
console.info('recycleNWeb, url = ' + url);
let baseNode = nodeMap.get(url);
if (!baseNode) {
console.info('no such node, url = ' + url);
return false;
}
// 检查组件是否被绑定
if (!force && baseNode.isBound()) {
console.info('the node is in bound and not force, can not delete');
return false;
}
// 释放资源
baseNode.rootNode?.dispose();
baseNode.rebuild(); // 触发重建,使NodeContainer更新
nodeMap.delete(url);
recycledNWebs.add(url); // 记录已释放
return true;
}
// 自定义释放所有离线Web组件的接口
export const recycleNWebs = (force: boolean = false) => {
nodeMap.forEach((_node: MyNodeController | undefined, url: ResourceStr) => {
recycleNWeb(url, force);
});
}
// 自定义恢复之前释放离线Web组件的接口
export const restoreNWebs = (uiContext: UIContext | undefined = undefined) => {
if (!uiContext) {
uiContext = globalUiContext;
}
for (let url of recycledNWebs) {
if (uiContext) {
createNWeb(url, uiContext); // 重新创建
}
}
recycledNWebs.clear() // 清空释放集合
}
// MyNodeController中需要添加isBound方法
export class MyNodeController extends NodeController {
private bound: boolean = false;
// ... 其他方法 ...
// 检查当前节点是否被绑定到NodeContainer
isBound(): boolean {
return this.bound;
}
// 可以通过aboutToAppear/aboutToDisappear跟踪绑定状态
aboutToAppear() {
console.info('aboutToAppear');
this.bound = true;
}
aboutToDisappear() {
console.info('aboutToDisappear');
this.bound = false;
}
}
更多推荐




所有评论(0)