1# Web组件开发性能提升指导 2<!--Kit: Common--> 3<!--Subsystem: Demo&Sample--> 4<!--Owner: @mgy917--> 5<!--Designer: @jiangwensai--> 6<!--Tester: @Lyuxin--> 7<!--Adviser: @huipeizi--> 8 9## 简介 10 11开发者实现在应用中跳转显示网页需要分为两个方面:使用@ohos.web.webview提供Web控制能力;使用Web组件提供网页显示的能力。在实际应用中往往由于各种原因导致首次跳转Web网页或Web组件内跳转时出现白屏、卡顿等情况。本文介绍提升Web首页加载与Web网页间跳转速度的几种方法,并提供[示例源码](https://gitcode.com/openharmony/applications_app_samples/tree/master/code/Performance/PerformanceLibrary/feature/webPerformance)。 12 13## 优化思路 14 15用户在使用Web组件显示网页时往往会经历四个阶段:无反馈-->白屏-->网页渲染-->完全展示,系统会在各个阶段内分别进行WebView初始化、建立网络连接、接受数据与渲染页面等操作,如图一所示是WebView的启动阶段。 16 17图一 Web组件显示页面的阶段 18 19 20 21要优化Web组件的首页加载性能,可以从图例标记的三个阶段来进行优化: 22 231. 在WebView的初始化阶段:应用打开WebView的第一步是启动浏览器内核,而这段时间由于WebView还不存在,所有后续的步骤是完全阻塞的。因此可以考虑在应用中预先完成初始化WebView,以及在初始化的同时通过预先加载组件内核、完成网络请求等方法,使得WebView初始化不是完全的阻塞后续步骤,从而减小耗时。 242. 在建立连接阶段:当开发者提前知道访问的网页地址,我们可以预先建立连接,进行DNS预解析。 253. 在接收资源数据阶段:当开发者预先知道用户下一页会点击什么页面的时候,可以合理使用缓存和预加载,将该页面的资源提前下载到缓存中。 26 27综上所述,开发者可以通过方法1和2来提升Web首页加载速度,在应用创建Ability的时候,在OnCreate阶段预先初始化内核。随后在onAppear阶段进行预解析DNS、预连接要加载的首页。 28在网页跳转的场景,开发者也可以通过方法3,在onPageEnd阶段预加载下一个要访问的页面,提升Web网页间的跳转和显示速度,如图二所示。 29 30图二 Web组件的生命周期回调函数 31 32 33 34## 优化方法 35 36### 提前初始化内核 37 38**原理介绍** 39 40当应用首次打开时,默认不会初始化浏览器内核,只有当创建WebView实例的时候,才会开始初始化浏览器内核。 41为了能提前初始化WebView实例,@ohos.web.webview提供了initializeWebEngine方法。该方法实现在Web组件初始化之前,通过接口加载Web引擎的动态库文件,从而提前进行Web组件动态库的加载和Web内核主进程的初始化,最终以提高启动性能,减少白屏时间。 42 43 44**实践案例** 45 46【反例】 47 48在未初始化Web内核前提下,启动加载Web页面。 49 50```typescript 51import web_webview from '@ohos.web.webview'; 52 53@Entry 54@Component 55struct Index { 56 controller: web_webview.WebviewController = new web_webview.WebviewController(); 57 58 build() { 59 Column() { 60 Web({ src: 'https://www.example.com/example.html', controller: this.controller }) 61 .fileAccess(true) 62 } 63 } 64} 65``` 66 67性能打点数据如下,getMessageData进程中的Duration为加载页面开始到结束的耗时: 68 69 70 71 72【正例】 73 74在页面开始加载时,调用initializeWebEngine()接口初始化Web内核,具体步骤如下: 75 761. 初始化Web内核 77 78```typescript 79// EntryAbility.ets 80import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit'; 81import { webview } from '@kit.ArkWeb'; 82 83export default class EntryAbility extends UIAbility { 84 onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { 85 webview.WebviewController.initializeWebEngine(); 86 } 87} 88``` 89 902. 加载Web组件 91 92```typescript 93// xxx.ets 94import web_webview from '@ohos.web.webview'; 95 96@Entry 97@Component 98struct Index { 99 controller: web_webview.WebviewController = new web_webview.WebviewController(); 100 101 build() { 102 Column() { 103 Web({ src: 'https://www.example.com/example.html', controller: this.controller }) 104 .fileAccess(true) 105 } 106 } 107} 108``` 109 110性能打点数据如下,getMessageData进程中的Duration为加载页面开始到结束的耗时: 111 112 113 114 115**总结** 116 117| **页面加载方式** | **耗时(局限不同设备和场景,数据仅供参考)** | **说明** | 118| ----------------- | ------------------------------------------ | ------------------------------------------------- | 119| 直接加载Web页面 | 1264ms | 在加载Web组件时才初始化Web内核,增加启动时间。 | 120| 提前初始化Web内核 | 1153ms | 加载页面时减少了Web内核初始化步骤,提高启动性能。 | 121 122 123### 预解析DNS、预连接 124WebView在onAppear阶段进行预连接socket, 当Web内核真正发起请求的时候会直接复用预连接的socket,如果当前预解析还没完成,真正发起网络请求进行DNS解析的时候也会复用当前正在执行的DNS解析任务。同理即使预连接的socket还没有连接成功,Web内核也会复用当前正在连接中的socket,进而优化资源的加载过程。 125@ohos.web.webview提供了prepareForPageLoad方法实现预连接url,在加载url之前调用此API,对url只进行DNS解析、socket建链操作,并不获取主资源子资源。 126参数: 127 128| 参数名 | 类型 | 说明 | 129| -------------- | ------- | ------------------------------------------------------------ | 130| url | string | 预连接的url。 | 131| preconnectable | boolean | 是否进行预连接。如果preconnectable为true,则对url进行dns解析,socket建链预连接;如果preconnectable为false,则不做任何预连接操作。 | 132| numSockets | number | 要预连接的socket数。socket数目连接需要大于0,最多允许6个连接。 | 133 134使用方法如下: 135 136```typescript 137// 开启预连接需要先使用上述方法预加载WebView内核。 138webview.WebviewController.initializeWebEngine(); 139// 启动预连接,连接地址为即将打开的网址。 140webview.WebviewController.prepareForPageLoad("https://www.example.com", true, 2); 141``` 142 143 144### 预加载下一页 145开发者可以在onPageEnd阶段进行预加载,当真正去加载下一个页面的时候,如果预加载已经成功,则相当于直接从缓存中加载页面资源,速度更快。一般来说能够准确预测到用户下一步要访问的页面的时候,可以进行预加载将要访问的页面,比如小说下一页, 浏览器在地址栏输入过程中识别到用户将要访问的页面等。 146@ohos.web.webview提供prefetchPage方法实现在预测到将要加载的页面之前调用,提前下载页面所需的资源,包括主资源子资源,但不会执行网页JavaScript代码或呈现网页,以加快加载速度。 147参数: 148 149| 参数名 | 类型 | 说明 | 150| ----------------- | ----------------- | --------------------- | 151| url | string | 预加载的url。 | 152| additionalHeaders | Array\<WebHeader> | url的附加HTTP请求头。 | 153 154使用方法如下: 155```typescript 156// src/main/ets/pages/WebBrowser.ets 157 158import { webview } from '@kit.ArkWeb'; 159 160@Entry 161@Component 162struct WebComponent { 163 controller: webview.WebviewController = new webview.WebviewController(); 164 165 build() { 166 Column() { 167 // ... 168 Web({ src: 'https://www.example.com', controller: this.controller }) 169 .onPageEnd((event) => { 170 // ... 171 // 在确定即将跳转的页面时开启预加载,url请替换真实地址。 172 this.controller.prefetchPage('https://www.example.com/nextpage'); 173 }) 174 .width('100%') 175 .height('80%') 176 177 Button('下一页') 178 .onClick(() => { 179 // ... 180 // 跳转下一页。 181 this.controller.loadUrl('https://www.example.com/nextpage'); 182 }) 183 } 184 } 185} 186``` 187 188### 预渲染优化 189 190**原理介绍** 191 192预渲染优化适用于Web页面启动和跳转场景,例如,进入首页后,跳转到其他子页。与预连接、预下载不同的是,预渲染需要开发者额外创建一个新的ArkWeb组件,并在后台对其进行预渲染,此时该组件并不会立刻挂载到组件树上,即不会对用户呈现(组件状态为Hidden和InActive),开发者可以在后续使用中按需动态挂载。 193 194具体原理如下图所示,首先需要定义一个自定义组件封装ArkWeb组件,该ArkWeb组件被离线创建,被包含在一个无状态的节点NodeContainer中,并与相应的NodeController绑定。该ArkWeb组件在后台完成预渲染后,在需要展示该ArkWeb组件时,再通过NodeController将其挂载到ViewTree的NodeContainer中,即通过NodeController绑定到对应的NodeContainer组件。预渲染通用实现的步骤如下: 195 196创建自定义ArkWeb组件:开发者需要根据实际场景创建封装一个自定义的ArkWeb组件,该ArkWeb组件被离线创建。 197创建并绑定NodeController:实现NodeController接口,用于自定义节点的创建、显示、更新等操作的管理。并将对应的NodeController对象放入到容器中,等待调用。 198绑定NodeContainer组件:将NodeContainer与NodeController进行绑定,实现动态组件页面显示。 199 200图三 预渲染优化原理图 201 202 203 204> **说明** 205> 206> 预渲染相比于预下载、预连接方案,会消耗更多的内存、算力,仅建议针对高频页面使用,单应用后台创建的ArkWeb组件要求小于200个。 207> 208> 在后台,预渲染的网页会持续进行渲染,为了防止发热和功耗问题,建议在预渲染完成后立即停止渲染过程。可以参考以下示例,使用 [onFirstMeaningfulPaint](../reference/apis-arkweb/arkts-basic-components-web-events.md#onfirstmeaningfulpaint12) 来确定预渲染的停止时机,该接口适用于http和https的在线网页。 209 210**实践案例** 211 2121. 创建载体,并创建ArkWeb组件。 213 ```typescript 214 // 载体Ability 215 // EntryAbility.ets 216 import {createNWeb} from "../pages/common"; 217 import { UIAbility } from '@kit.AbilityKit'; 218 import { window } from '@kit.ArkUI'; 219 220 export default class EntryAbility extends UIAbility { 221 onWindowStageCreate(windowStage: window.WindowStage): void { 222 windowStage.loadContent('pages/Index', (err, data) => { 223 // 创建ArkWeb动态组件(需传入UIContext),loadContent之后的任意时机均可创建。 224 createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext()); 225 if (err.code) { 226 return; 227 } 228 }); 229 } 230 } 231 ``` 2322. 创建NodeContainer和对应的NodeController,渲染后台ArkWeb组件。 233 234 ```typescript 235 // 创建NodeController。 236 // common.ets 237 import { UIContext } from '@kit.ArkUI'; 238 import { webview } from '@kit.ArkWeb'; 239 import { NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI'; 240 // @Builder中为动态组件的具体组件内容。 241 // Data为入参封装类。 242 class Data{ 243 url: string = 'https://www.example.com'; 244 controller: WebviewController = new webview.WebviewController(); 245 } 246 // 通过布尔变量shouldInactive控制网页在后台完成预渲染后停止渲染。 247 let shouldInactive: boolean = true; 248 @Builder 249 function WebBuilder(data:Data) { 250 Column() { 251 Web({ src: data.url, controller: data.controller }) 252 .onPageBegin(() => { 253 // 调用onActive,开启渲染。 254 data.controller.onActive(); 255 }) 256 .onFirstMeaningfulPaint(() =>{ 257 if (!shouldInactive) { 258 return; 259 } 260 // 在预渲染完成时触发,停止渲染。 261 data.controller.onInactive(); 262 shouldInactive = false; 263 }) 264 .width("100%") 265 .height("100%") 266 } 267 } 268 let wrap = wrapBuilder<Data[]>(WebBuilder); 269 // 用于控制和反馈对应的NodeContianer上的节点的行为,需要与NodeContainer一起使用。 270 export class myNodeController extends NodeController { 271 private rootnode: BuilderNode<Data[]> | null = null; 272 // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContianer中。 273 // 在对应NodeContianer创建的时候调用、或者通过rebuild方法调用刷新。 274 makeNode(uiContext: UIContext): FrameNode | null { 275 console.info(" uicontext is undifined : "+ (uiContext === undefined)); 276 if (this.rootnode != null) { 277 // 返回FrameNode节点。 278 return this.rootnode.getFrameNode(); 279 } 280 // 返回null控制动态组件脱离绑定节点。 281 return null; 282 } 283 // 当布局大小发生变化时进行回调。 284 aboutToResize(size: Size) { 285 console.info("aboutToResize width : " + size.width + " height : " + size.height ); 286 } 287 // 当controller对应的NodeContainer在Appear的时候进行回调。 288 aboutToAppear() { 289 console.info("aboutToAppear"); 290 // 切换到前台后,不需要停止渲染。 291 shouldInactive = false; 292 } 293 // 当controller对应的NodeContainer在Disappear的时候进行回调。 294 aboutToDisappear() { 295 console.info("aboutToDisappear"); 296 } 297 // 此函数为自定义函数,可作为初始化函数使用。 298 // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容。 299 initWeb(url:string, uiContext:UIContext, control:WebviewController) { 300 if(this.rootnode != null){ 301 return; 302 } 303 // 创建节点,需要uiContext。 304 this.rootnode = new BuilderNode(uiContext); 305 // 创建动态Web组件。 306 this.rootnode.build(wrap, { url:url, controller:control }); 307 } 308 } 309 // 创建Map保存所需要的NodeController。 310 let NodeMap:Map<string, myNodeController | undefined> = new Map(); 311 // 创建Map保存所需要的WebViewController。 312 let controllerMap:Map<string, WebviewController | undefined> = new Map(); 313 // 初始化需要UIContext 需在Ability获取。 314 export const createNWeb = (url: string, uiContext: UIContext) => { 315 // 创建NodeController。 316 let baseNode = new myNodeController(); 317 let controller = new webview.WebviewController() ; 318 // 初始化自定义Web组件。 319 baseNode.initWeb(url, uiContext, controller); 320 controllerMap.set(url, controller); 321 NodeMap.set(url, baseNode); 322 } 323 // 自定义获取NodeController接口。 324 export const getNWeb = (url : string) : myNodeController | undefined => { 325 return NodeMap.get(url); 326 } 327 ``` 3283. 通过NodeContainer使用已经预渲染的页面。 329 330 ```typescript 331 // 使用NodeController的Page页。 332 // Index.ets 333 import {createNWeb, getNWeb} from "./common"; 334 335 @Entry 336 @Component 337 struct Index { 338 build() { 339 Row() { 340 Column() { 341 // NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode。 342 // Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示。 343 NodeContainer(getNWeb("https://www.example.com")) 344 .height("90%") 345 .width("100%") 346 } 347 .width('100%') 348 } 349 .height('100%') 350 } 351 } 352 ``` 353 354 355### 预取POST请求优化 356 357**原理介绍** 358 359预取POST请求适用于Web页面启动和跳转场景,当即将加载的Web页面中存在POST请求且POST请求耗时较长时,会导致页面加载时间增加,可以选择不同时机对POST请求进行预取,消除等待POST请求数据下载完成的耗时,具体有以下两种场景可供参考: 360 3611. 如果是应用首页,推荐在ArkWeb组件创建后或者提前初始化Web内核后,对首页的POST请求进行预取,如onCreate、aboutToAppear。 3622. 当前页面完成加载后,可以对用户下一步可能点击页面的POST请求进行预取,推荐在Web组件的生命周期函数onPageEnd及后继时机进行。 363 364注意事项: 365 3661. 本方案能消除POST请求下载耗时,预计收益可能在百毫秒(依赖POST请求的数据内容和当前网络环境)。 3672. 预取POST请求行为包括连接和资源下载,连接和资源加载耗时可能达到百毫秒(依赖POST请求的数据内容和当前网络环境),建议开发者为预下载留出足够的时间。 3683. 预取POST请求行为相比于预连接会消耗额外的流量、内存,建议针对高频页面使用。 3694. POST请求具有一定的即时性,预取POST请求需要指定恰当的有效期。 3705. 目前仅支持预取Context-Type为application/x-www-form-urlencoded的POST请求。最多可以预获取6个POST请求。如果要预获取第7个,会自动清除最早预获取的POST缓存。开发者也可以通过clearPrefetchedResource()接口主动清除后续不再使用的预获取资源缓存。 3716. 如果要使用预获取的资源缓存,开发者需要在正式发起的POST请求的请求头中增加键值“ArkWebPostCacheKey”,其内容为对应缓存的cacheKey。 372 373 374**案例实践** 375 376 377**场景一:加载包含POST请求的首页** 378 379【不推荐用法】 380 381当首页中包含POST请求,且POST请求耗时较长时,不推荐直接加载Web页面。 382 383```typescript 384// xxx.ets 385import { webview } from '@kit.ArkWeb'; 386 387@Entry 388@Component 389struct WebComponent { 390 webviewController: webview.WebviewController = new webview.WebviewController(); 391 392 build() { 393 Column() { 394 Web({ src: 'https://www.example.com/', controller: this.webviewController }) 395 } 396 } 397} 398``` 399 400 401【推荐用法】 402 403通过预取POST加载包含POST请求的首页,具体步骤如下: 404 4051. 通过initializeWebEngine()来提前初始化Web组件的内核,然后在初始化内核后调用prefetchResource()预获取将要加载页面中的POST请求。 406 407```typescript 408// EntryAbility.ets 409import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit'; 410import { webview } from '@kit.ArkWeb'; 411 412export default class EntryAbility extends UIAbility { 413 onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 414 console.info('EntryAbility onCreate.'); 415 webview.WebviewController.initializeWebEngine(); 416 // 预获取时,需要将"https://www.example1.com/POST?e=f&g=h"替换成为真实要访问的网站地址。 417 webview.WebviewController.prefetchResource( 418 { 419 url: 'https://www.example.com/POST?e=f&g=h', 420 method: 'POST', 421 formData: 'a=x&b=y' 422 }, 423 [{ 424 headerKey: 'c', 425 headerValue: 'z' 426 }], 427 'KeyX', 500 428 ); 429 AppStorage.setOrCreate('abilityWant', want); 430 console.info('EntryAbility onCreate done.'); 431 } 432} 433``` 434 4352. 通过Web组件,加载包含POST请求的Web页面。 436 437```typescript 438// xxx.ets 439import { webview } from '@kit.ArkWeb'; 440 441@Entry 442@Component 443struct WebComponent { 444 webviewController: webview.WebviewController = new webview.WebviewController(); 445 446 build() { 447 Column() { 448 Web({ src: 'https://www.example.com/', controller: this.webviewController }) 449 .onPageEnd(() => { 450 // 清除后续不再使用的预获取资源缓存 451 webview.WebviewController.clearPrefetchedResource(['KeyX']); 452 }) 453 } 454 } 455} 456``` 457 4583. 在页面将要加载的JavaScript文件中,发起POST请求,设置请求响应头ArkWebPostCacheKey为对应预取时设置的cachekey值'KeyX'。 459 460```typescript 461const xhr = new XMLHttpRequest(); 462xhr.open('POST', 'https://www.example.com/POST?e=f&g=h', true); 463xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 464xhr.setRequestHeader('ArkWebPostCacheKey', 'KeyX'); 465xhr.onload = function () { 466 if (xhr.status >= 200 && xhr.status < 300) { 467 console.info('成功', xhr.responseText); 468 } else { 469 console.error('请求失败'); 470 } 471} 472const formData = new FormData(); 473formData.append('a', 'x'); 474formData.append('b', 'y'); 475xhr.send(formData); 476``` 477 478 479**场景二:加载包含POST请求的下一页** 480 481【不推荐用法】 482 483当即将加载的Web页面中包含POST请求,且POST请求耗时较长时,不推荐直接加载Web页面。 484 485```typescript 486// xxx.ets 487import { webview } from '@kit.ArkWeb'; 488 489@Entry 490@Component 491struct WebComponent { 492 webviewController: webview.WebviewController = new webview.WebviewController(); 493 494 build() { 495 Column() { 496 // 在适当的时机加载业务用Web组件,本例以Button点击触发为例。 497 Button('加载页面') 498 .onClick(() => { 499 // url请替换为真实地址。 500 this.webviewController.loadUrl('https://www.example1.com/'); 501 }) 502 Web({ src: 'https://www.example.com/', controller: this.webviewController }) 503 } 504 } 505} 506``` 507 508 509【推荐用法】 510 511通过预取POST加载包含POST请求的下一个跳转页面,具体步骤如下: 512 5131. 当前页面完成显示后,使用onPageEnd()对即将要加载页面中的POST请求进行预获取。 514 515```typescript 516// xxx.ets 517import { webview } from '@kit.ArkWeb'; 518 519@Entry 520@Component 521struct WebComponent { 522 webviewController: webview.WebviewController = new webview.WebviewController(); 523 524 build() { 525 Column() { 526 // 在适当的时机加载业务用Web组件,本例以Button点击触发为例。 527 Button('加载页面') 528 .onClick(() => { 529 // url请替换为真实地址。 530 this.webviewController.loadUrl('https://www.example1.com/'); 531 }) 532 Web({ src: 'https://www.example.com/', controller: this.webviewController }) 533 .onPageEnd(() => { 534 // 预获取时,需要将"https://www.example1.com/POST?e=f&g=h"替换成为真实要访问的网站地址。 535 webview.WebviewController.prefetchResource( 536 { 537 url: 'https://www.example1.com/POST?e=f&g=h', 538 method: 'POST', 539 formData: 'a=x&b=y' 540 }, 541 [{ 542 headerKey: 'c', 543 headerValue: 'z' 544 }], 545 'KeyX', 500 546 ); 547 }) 548 } 549 } 550} 551``` 552 5532. 将要加载的页面中,js正式发起POST请求,设置请求响应头ArkWebPostCacheKey为对应预取时设置的cachekey值'KeyX'。 554 555```typescript 556const xhr = new XMLHttpRequest(); 557xhr.open('POST', 'https://www.example1.com/POST?e=f&g=h', true); 558xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 559xhr.setRequestHeader('ArkWebPostCacheKey', 'KeyX'); 560xhr.onload = function () { 561 if (xhr.status >= 200 && xhr.status < 300) { 562 console.info('成功', xhr.responseText); 563 } else { 564 console.error('请求失败'); 565 } 566} 567const formData = new FormData(); 568formData.append('a', 'x'); 569formData.append('b', 'y'); 570xhr.send(formData); 571``` 572 573 574### JSBridge优化 575 576**适用场景** 577 578应用使用ArkTS、C++语言混合开发,或本身应用架构较贴近于小程序架构,自带C++侧环境, 579推荐使用ArkWeb在native侧提供的ArkWeb_ControllerAPI、ArkWeb_ComponentAPI实现JSBridge功能。 580 581 582上图为具有普适性的小程序一般架构,其中逻辑层需要应用自带JavaScript运行时,本身已存在C++环境,通过native接口可直接在C++环境中完成与视图层(ArkWeb作为渲染器)的通信,无需再返回ArkTS环境调用JSBridge相关接口。 583 584Native JSBridge方案可以解决ArkTS环境的冗余切换,同时允许回调在非UI线程上报,避免造成UI阻塞。 585 586**案例实践** 587 588【反例】 589 590使用ArkTS接口实现JSBridge通信。 591 592应用侧代码: 593```typescript 594import { webview } from '@kit.ArkWeb'; 595 596@Entry 597@Component 598struct WebComponent { 599 webviewController: webview.WebviewController = new webview.WebviewController(); 600 601 aboutToAppear() { 602 // 配置Web开启调试模式。 603 webview.WebviewController.setWebDebuggingAccess(true); 604 } 605 606 build() { 607 Column() { 608 Button('runJavaScript') 609 .onClick(() => { 610 console.info(`现在时间是:${new Date().getTime()}`); 611 // 前端页面函数无参时,将param删除。 612 this.webviewController.runJavaScript('htmlTest(param)'); 613 }) 614 Button('runJavaScriptCodePassed') 615 .onClick(() => { 616 // 传递runJavaScript侧代码方法。 617 this.webviewController.runJavaScript(`function changeColor(){document.getElementById('text').style.color = 'red'}`); 618 }) 619 Web({ src: $rawfile('index.html'), controller: this.webviewController }) 620 } 621 } 622} 623``` 624 625加载的html文件: 626```html 627<!DOCTYPE html> 628<html> 629<body> 630<button type="button" onclick="callArkTS()">Click Me!</button> 631<h1 id="text">这是一个测试信息,默认字体为黑色,调用runJavaScript方法后字体为绿色,调用runJavaScriptCodePassed方法后字体为红色</h1> 632<script> 633 // 调用有参函数时实现。 634 var param = "param: JavaScript Hello World!"; 635 function htmlTest(param) { 636 document.getElementById('text').style.color = 'green'; 637 document.getElementById('text').innerHTML = `现在时间:${new Date().getTime()}`; 638 console.info(param); 639 } 640 // 调用无参函数时实现。 641 function htmlTest() { 642 document.getElementById('text').style.color = 'green'; 643 } 644 // Click Me!触发前端页面callArkTS()函数执行JavaScript传递的代码。 645 function callArkTS() { 646 changeColor(); 647 } 648</script> 649</body> 650</html> 651``` 652 653点击runJavaScript按钮后触发h5页面htmlTest方法,使得页面内容变更为当前时间戳,如下图所示: 654 655 656 657 658 659经过多轮测试,可以得出从点击原生button到h5触发htmlTest方法,耗时约7ms~9ms。 660 661【正例】 662 663使用NDK接口实现JSBridge通信。 664 665应用侧代码: 666```typescript 667import testNapi from 'libentry.so'; 668import { webview } from '@kit.ArkWeb'; 669 670class testObj { 671 test(): string { 672 console.info('ArkUI Web Component'); 673 return "ArkUI Web Component"; 674 } 675 676 toString(): void { 677 console.info('Web Component toString'); 678 } 679} 680 681@Entry 682@Component 683struct Index { 684 webTag: string = 'ArkWeb1'; 685 controller: webview.WebviewController = new webview.WebviewController(this.webTag); 686 @State testObjtest: testObj = new testObj(); 687 688 aboutToAppear() { 689 console.info("aboutToAppear"); 690 //初始化web ndk。 691 testNapi.nativeWebInit(this.webTag); 692 } 693 694 build() { 695 Column() { 696 Row() { 697 Button('runJS hello') 698 .fontSize(12) 699 .onClick(() => { 700 console.info(`start:---->new Date().getTime()`); 701 testNapi.runJavaScript(this.webTag, "runJSRetStr(\"" + "hello" + "\")"); 702 }) 703 }.height('20%') 704 705 Row() { 706 Web({ src: $rawfile('runJS.html'), controller: this.controller }) 707 .javaScriptAccess(true) 708 .fileAccess(true) 709 .onControllerAttached(() => { 710 console.info(`${this.controller.getWebId()}`); 711 }) 712 }.height('80%') 713 } 714 } 715} 716``` 717 718hello.cpp作为应用C++侧业务逻辑代码: 719```C 720//注册对象及方法,发送脚本到H5执行后的回调,解析存储应用侧传过来的实例等代码逻辑这里不进行展示,开发者根据自身业务场景自行实现。 721 722// 发送JS脚本到H5侧执行。 723static napi_value RunJavaScript(napi_env env, napi_callback_info info) { 724 size_t argc = 2; 725 napi_value args[2] = {nullptr}; 726 napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); 727 728 // 获取第一个参数 webTag。 729 size_t webTagSize = 0; 730 napi_get_value_string_utf8(env, args[0], nullptr, 0, &webTagSize); 731 char *webTagValue = new (std::nothrow) char[webTagSize + 1]; 732 size_t webTagLength = 0; 733 napi_get_value_string_utf8(env, args[0], webTagValue, webTagSize + 1, &webTagLength); 734 OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", "ndk OH_NativeArkWeb_RunJavaScript webTag:%{public}s", 735 webTagValue); 736 737 // 获取第二个参数 jsCode。 738 size_t bufferSize = 0; 739 napi_get_value_string_utf8(env, args[1], nullptr, 0, &bufferSize); 740 char *jsCode = new (std::nothrow) char[bufferSize + 1]; 741 size_t byteLength = 0; 742 napi_get_value_string_utf8(env, args[1], jsCode, bufferSize + 1, &byteLength); 743 744 OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", 745 "ndk OH_NativeArkWeb_RunJavaScript jsCode len:%{public}zu", strlen(jsCode)); 746 747 // 构造runJS执行的结构体。 748 ArkWeb_JavaScriptObject object = {(uint8_t *)jsCode, bufferSize, &JSBridgeObject::StaticRunJavaScriptCallback, 749 static_cast<void *>(jsbridge_object_ptr->GetWeakPtr())}; 750 controller->runJavaScript(webTagValue, &object); 751 return nullptr; 752} 753 754EXTERN_C_START 755static napi_value Init(napi_env env, napi_value exports) { 756 napi_property_descriptor desc[] = { 757 {"nativeWebInit", nullptr, NativeWebInit, nullptr, nullptr, nullptr, napi_default, nullptr}, 758 {"runJavaScript", nullptr, RunJavaScript, nullptr, nullptr, nullptr, napi_default, nullptr}, 759 }; 760 napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); 761 return exports; 762} 763EXTERN_C_END 764 765static napi_module demoModule = { 766 .nm_version = 1, 767 .nm_flags = 0, 768 .nm_filename = nullptr, 769 .nm_register_func = Init, 770 .nm_modname = "entry", 771 .nm_priv = ((void *)0), 772 .reserved = {0}, 773}; 774 775extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(&demoModule); } 776``` 777 778Native侧业务代码entry/src/main/cpp/jsbridge_object.h、entry/src/main/cpp/jsbridge_object.cpp 779详见[应用侧与前端页面的相互调用(C/C++)](../web/arkweb-ndk-jsbridge.md)。 780 781runJS.html作为应用前端页面: 782 783```html 784<!DOCTYPE html> 785<html lang="en-gb"> 786<head> 787 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 788 <title>run javascript demo</title> 789</head> 790<body> 791<h1>run JavaScript Ext demo</h1> 792<p id="webDemo"></p> 793<br> 794<button type="button" style="height:30px;width:200px" onclick="testNdkProxyObjMethod1()">test ndk method1 ! </button> 795<br> 796<br> 797<button type="button" style="height:30px;width:200px" onclick="testNdkProxyObjMethod2()">test ndk method2 ! </button> 798<br> 799 800</body> 801<script type="text/javascript"> 802 803 function testNdkProxyObjMethod1() { 804 805 // 校验ndk方法是否已经注册到window。 806 if (window.ndkProxy == undefined) { 807 document.getElementById("webDemo").innerHTML = "ndkProxy undefined" 808 return "objName undefined" 809 } 810 811 if (window.ndkProxy.method1 == undefined) { 812 document.getElementById("webDemo").innerHTML = "ndkProxy method1 undefined" 813 return "objName test undefined" 814 } 815 816 if (window.ndkProxy.method2 == undefined) { 817 document.getElementById("webDemo").innerHTML = "ndkProxy method2 undefined" 818 return "objName test undefined" 819 } 820 821 // 调用ndk注册到window的method1方法,并将结果回显到p标签。 822 var retStr = window.ndkProxy.method1("hello", "world", [1.2, -3.4, 123.456], ["Saab", "Volvo", "BMW", undefined], 1.23456, 123789, true, false, 0, undefined); 823 document.getElementById("webDemo").innerHTML = "ndkProxy and method1 is ok, " + retStr; 824 } 825 826 function testNdkProxyObjMethod2() { 827 828 // 校验ndk方法是否已经注册到window。 829 if (window.ndkProxy == undefined) { 830 document.getElementById("webDemo").innerHTML = "ndkProxy undefined" 831 return "objName undefined" 832 } 833 834 if (window.ndkProxy.method1 == undefined) { 835 document.getElementById("webDemo").innerHTML = "ndkProxy method1 undefined" 836 return "objName test undefined" 837 } 838 839 if (window.ndkProxy.method2 == undefined) { 840 document.getElementById("webDemo").innerHTML = "ndkProxy method2 undefined" 841 return "objName test undefined" 842 } 843 844 var student = { 845 name:"zhang", 846 sex:"man", 847 age:25 848 }; 849 var cars = [student, 456, false, 4.567]; 850 let params = "[\"{\\\"scope\\\"]"; 851 852 // 调用ndk注册到window的method2方法,并将结果回显到p标签。 853 var retStr = window.ndkProxy.method2("hello", "world", false, cars, params); 854 document.getElementById("webDemo").innerHTML = "ndkProxy and method2 is ok, " + retStr; 855 } 856 857 function runJSRetStr(data) { 858 const d = new Date(); 859 let time = d.getTime(); 860 document.getElementById("webDemo").innerHTML = new Date().getTime() 861 return JSON.stringify(time) 862 } 863</script> 864</html> 865``` 866 867点击runJS hello按钮后触发h5页面runJSRetStr方法,使得页面内容变更为当前时间戳。 868 869 870 871 872 873经过多轮测试,可以得出从点击原生button到h5触发runJSRetStr方法,耗时约2ms~6ms。 874 875 876**总结** 877 878| **通信方式** | **耗时(局限不同设备和场景,数据仅供参考)** | **说明** | 879| ----------------------------- | ------------------------------------------ | ------------------------------- | 880| ArkWeb实现与前端页面通信 | 7ms~9ms | ArkTS环境冗余切换,耗时较长。 | 881| ArkWeb、c++实现与前端页面通信 | 2ms~6ms | 避免ArkTS环境冗余切换,耗时短。 | 882 883JSBridge优化方案适用于ArkWeb应用侧与前端网页通信场景,开发者可根据应用架构选择合适的业务通信机制: 884 8851.应用使用ArkTS语言开发,推荐使用ArkWeb在ArkTS提供的runJavaScriptExt接口实现应用侧至前端页面的通信,同时使用registerJavaScriptProxy实现前端页面至应用侧的通信。 886 8872.应用使用ArkTS、C++语言混合开发,或本身应用结构较贴近于小程序架构,自带C++侧环境,推荐使用ArkWeb在NDK侧提供的OH_NativeArkWeb_RunJavaScript及OH_NativeArkWeb_RegisterJavaScriptProxy接口实现JSBridge功能。 888 889> 说明 890> 开发者需根据当前业务区分是否存在C++侧环境(较为显著标志点为当前应用是否使用了Node API技术进行开发,若是则该应用具备C++侧环境)。 891> 具备C++侧环境的应用开发,可使用ArkWeb提供的NDK侧JSBridge接口。 892> 不具备C++侧环境的应用开发,可使用ArkWeb侧JSBridge接口。 893 894 895### 异步JSBridge调用 896 897**原理介绍** 898 899异步JSBridge调用适用于H5侧调用原生或C++侧注册得JSBridge函数场景下,将用户指定的JSBridge接口的调用抛出后,不等待执行结果, 900以避免在ArkUI主线程负载重时JSBridge同步调用可能导致Web线程等待IPC时间过长,从而造成阻塞的问题。 901 902**实践案例** 903 904使用ArkTS接口实现JSBridge通信。 905 906【案例一】 907 908步骤1.只注册同步函数。 909```typescript 910import webview from '@ohos.web.webview'; 911import { BusinessError } from '@kit.BasicServicesKit'; 912 913// 定义ETS侧对象及函数。 914class TestObj { 915 test(testStr:string): string { 916 let start = Date.now(); 917 // 模拟耗时操作。 918 for(let i = 0; i < 500000; i++) {} 919 let end = Date.now(); 920 console.info('objName.test start: ' + start); 921 return 'objName.test Sync function took ' + (end - start) + 'ms'; 922 } 923 asyncTestBool(testBol:boolean): Promise<string> { 924 return new Promise((resolve, reject) => { 925 let start = Date.now(); 926 // 模拟耗时操作(异步)。 927 setTimeout(() => { 928 for(let i = 0; i < 500000; i++) {} 929 let end = Date.now(); 930 console.info('objAsyncName.asyncTestBool start: ' + start); 931 resolve('objName.asyncTestBool Async function took ' + (end - start) + 'ms'); 932 }, 0); // 使用0毫秒延迟来模拟立即开始的异步操作。 933 }); 934 } 935} 936 937class WebObj { 938 webTest(): string { 939 let start = Date.now(); 940 // 模拟耗时操作。 941 for(let i = 0; i < 500000; i++) {} 942 let end = Date.now(); 943 console.info('objTestName.webTest start: ' + start); 944 return 'objTestName.webTest Sync function took ' + (end - start) + 'ms'; 945 } 946 webString(): string { 947 let start = Date.now(); 948 // 模拟耗时操作。 949 for(let i = 0; i < 500000; i++) {} 950 let end = Date.now(); 951 console.info('objTestName.webString start: ' + start); 952 return 'objTestName.webString Sync function took ' + (end - start) + 'ms' 953 } 954} 955 956class AsyncObj { 957 958 asyncTest(): Promise<string> { 959 return new Promise((resolve, reject) => { 960 let start = Date.now(); 961 // 模拟耗时操作(异步)。 962 setTimeout(() => { 963 for (let i = 0; i < 500000; i++) { 964 } 965 let end = Date.now(); 966 console.info('objAsyncName.asyncTest start: ' + start); 967 resolve('objAsyncName.asyncTest Async function took ' + (end - start) + 'ms'); 968 }, 0); // 使用0毫秒延迟来模拟立即开始的异步操作。 969 }); 970 } 971 972 asyncString(testStr:string): Promise<string> { 973 return new Promise((resolve, reject) => { 974 let start = Date.now(); 975 // 模拟耗时操作(异步)。 976 setTimeout(() => { 977 for (let i = 0; i < 500000; i++) { 978 } 979 let end = Date.now(); 980 console.info('objAsyncName.asyncString start: ' + start); 981 resolve('objAsyncName.asyncString Async function took ' + (end - start) + 'ms'); 982 }, 0); // 使用0毫秒延迟来模拟立即开始的异步操作。 983 }); 984 } 985} 986 987@Entry 988@Component 989struct Index { 990 controller: webview.WebviewController = new webview.WebviewController(); 991 @State testObjtest: TestObj = new TestObj(); 992 @State webTestObj: WebObj = new WebObj(); 993 @State asyncTestObj: AsyncObj = new AsyncObj(); 994 build() { 995 Column() { 996 Button('refresh') 997 .onClick(()=>{ 998 try{ 999 this.controller.refresh(); 1000 } catch (error) { 1001 console.error(`ErrorCode:${(error as BusinessError).code},Message:${(error as BusinessError).message}`); 1002 } 1003 }) 1004 Button('Register JavaScript To Window') 1005 .onClick(()=>{ 1006 try { 1007 // 只注册同步函数。 1008 this.controller.registerJavaScriptProxy(this.webTestObj,"objTestName",["webTest","webString"]); 1009 } catch (error) { 1010 console.error(`ErrorCode:${(error as BusinessError).code},Message:${(error as BusinessError).message}`); 1011 } 1012 }) 1013 Web({src: $rawfile('index.html'),controller: this.controller}).javaScriptAccess(true) 1014 } 1015 } 1016} 1017``` 1018 1019步骤2.H5侧调用JSBridge函数。 1020```html 1021<!DOCTYPE html> 1022<html lang="en"> 1023<head> 1024 <meta charset="UTF-8"> 1025 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 1026 <title>Document</title> 1027</head> 1028<body> 1029<button type="button" onclick="htmlTest()"> Click Me!</button> 1030<p id="demo"></p> 1031<p id="webDemo"></p> 1032<p id="asyncDemo"></p> 1033</body> 1034<script type="text/javascript"> 1035 async function htmlTest() { 1036 document.getElementById("demo").innerHTML = `测试开始:${new Date().getTime()}\n`; 1037 1038 const time1 = new Date().getTime() 1039 objTestName.webString(); 1040 const time2 = new Date().getTime() 1041 1042 objAsyncName.asyncString() 1043 const time3 = new Date().getTime() 1044 1045 objName.asyncTestBool() 1046 const time4 = new Date().getTime() 1047 1048 objName.test(); 1049 const time5 = new Date().getTime() 1050 1051 objTestName.webTest(); 1052 const time6 = new Date().getTime() 1053 objAsyncName.asyncTest() 1054 const time7 = new Date().getTime() 1055 1056 const result = [ 1057 'objTestName.webString()耗时:'+ (time2 - time1), 1058 'objAsyncName.asyncString()耗时:'+ (time3 - time2), 1059 'objName.asyncTestBool()耗时:'+ (time4 - time3), 1060 'objName.test()耗时:'+ (time5 - time4), 1061 'objTestName.webTest()耗时:'+ (time6 - time5), 1062 'objAsyncName.asyncTest()耗时:'+ (time7 - time6), 1063 ] 1064 document.getElementById("demo").innerHTML = document.getElementById("demo").innerHTML + '\n' + result.join('\n') 1065 } 1066</script> 1067</html> 1068``` 1069 1070【案例二】 1071 1072步骤1.使用registerJavaScriptProxy或javaScriptProxy注册异步函数或异步同步共存。 1073```typescript 1074// registerJavaScriptProxy方式注册。 1075Button('refresh') 1076 .onClick(()=>{ 1077 try{ 1078 this.controller.refresh(); 1079 } catch (error) { 1080 console.error(`ErrorCode:${(error as BusinessError).code},Message:${(error as BusinessError).message}`); 1081 } 1082 }) 1083Button('Register JavaScript To Window') 1084 .onClick(()=>{ 1085 try { 1086 // 调用注册接口对象及成员函数,其中同步函数列表必填,空白则需要用[]占位;异步函数列表非必填。 1087 // 同步、异步函数都注册。 1088 this.controller.registerJavaScriptProxy(this.testObjtest,"objName",["test"],["asyncTestBool"]); 1089 // 只注册异步函数,同步函数列表处留空。 1090 this.controller.registerJavaScriptProxy(this.asyncTestObj,"objAsyncName",[],["asyncTest","asyncString"]); 1091 } catch (error) { 1092 console.error(`ErrorCode:${(error as BusinessError).code},Message:${(error as BusinessError).message}`); 1093 } 1094 }) 1095Web({src: $rawfile('index.html'),controller: this.controller}).javaScriptAccess(true) 1096 1097// javaScriptProxy方式注册。 1098// javaScriptProxy只支持注册一个对象,若需要注册多个对象请使用registerJavaScriptProxy。 1099Web({src: $rawfile('index.html'),controller: this.controller}) 1100 .javaScriptAccess(true) 1101 .javaScriptProxy({ 1102 object: this.testObjtest, 1103 name:"objName", 1104 methodList: ["test","toString"], 1105 // 指定异步函数列表。 1106 asyncMethodList: ["test","toString"], 1107 controller: this.controller 1108 }) 1109``` 1110 1111步骤2.H5侧调用JSBridge函数与反例中一致。 1112 1113**总结** 1114 1115 1116 1117| **注册方法类型** | **耗时(局限不同设备和场景,数据仅供参考)** | **说明** | 1118| ---------------- | ------------------------------------------ | ------------------------ | 1119| 同步方法 | 1398ms,2707ms,2705ms | 同步函数调用会阻塞JS线程 | 1120| 异步方法 | 2ms,2ms,4ms | 异步函数调用不阻塞JS线程 | 1121 1122通过截图可看到async的异步方法不需要等待结果,所以在JS单线程任务队列中不会长时间占用,同步任务需要等待原生主线程同步执行后返回结果。 1123 1124>JSBridge接口在注册时,即会根据注册调用的接口决定其调用方式(同步/异步)。开发者需根据当前业务区分, 1125> 是否将其注册为异步函数。 1126>- 同步函数调用将会阻塞JS的执行,等待调用的JSBridge函数执行结束,适用于需要返回值,或者有时序问题等场景。 1127>- 异步函数调用时不会等待JSBridge函数执行结束,后续JS可在短时间后继续执行。但JSBridge函数无法直接返回值。 1128>- 注册在ETS侧的JSBridge函数调用时需要在主线程上执行;NDK侧注册的函数将在其他线程中执行。 1129>- 异步JSBridge接口与同步接口在JS侧的调用方式一致,仅注册方式不同,本部分调用方式仅作简要示范。 1130 1131附NDK接口实现JSBridge通信(C++侧注册异步函数): 1132```c 1133// 定义JSBridge函数。 1134static void ProxyMethod1(const char* webTag, void* userData) { 1135 OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", "Method1 webTag :%{public}s",webTag); 1136} 1137 1138static void ProxyMethod2(const char* webTag, void* userData) { 1139 OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", "Method2 webTag :%{public}s",webTag); 1140} 1141 1142static void ProxyMethod3(const char* webTag, void* userData) { 1143 OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", "Method3 webTag :%{public}s",webTag); 1144} 1145 1146void RegisterCallback(const char *webTag) { 1147 int myUserData = 100; 1148 // 创建函数方法结构体。 1149 ArkWeb_ProxyMethod m1 = { 1150 .methodName = "method1", 1151 .callback = ProxyMethod1, 1152 .userData = (void *)&myUserData 1153 }; 1154 ArkWeb_ProxyMethod m2 = { 1155 .methodName = "method2", 1156 .callback = ProxyMethod2, 1157 .userData = (void *)&myUserData 1158 }; 1159 ArkWeb_ProxyMethod m3 = { 1160 .methodName = "method3", 1161 .callback = ProxyMethod3, 1162 .userData = (void *)&myUserData 1163 }; 1164 ArkWeb_ProxyMethod methodList[2] = {m1,m2}; 1165 1166 // 创建JSBridge对象结构体。 1167 ArkWeb_ProxyObject obj = { 1168 .objName = "ndkProxy", 1169 .methodList = methodList, 1170 .size = 2, 1171 }; 1172 // 获取ArkWeb_Controller API结构体。 1173 ArkWeb_AnyNativeAPI* apis = OH_ArkWeb_GetNativeAPI(ArkWeb_NativeAPIVariantKind::ARKWEB_NATIVE_CONTROLLER); 1174 ArkWeb_ControllerAPI* ctrlApi = reinterpret_cast<ArkWeb_ControllerAPI*>(apis); 1175 1176 // 调用注册接口,注册函数。 1177 ctrlApi->registerJavaScriptProxy(webTag, &obj); 1178 1179 ArkWeb_ProxyMethod asyncMethodList[1] = {m3}; 1180 ArkWeb_ProxyObject obj2 = { 1181 .objName = "ndkProxy", 1182 .methodList = asyncMethodList, 1183 .size = 1, 1184 }; 1185 ctrlApi->registerAsyncJavaScriptProxy(webTag, &obj2) 1186} 1187``` 1188 1189 1190### 预编译JavaScript生成字节码缓存(Code Cache) 1191 1192**原理介绍** 1193 1194预编译JavaScript生成字节码缓存适用于在页面加载之前提前将即将使用到的JavaScript文件编译成字节码并缓存到本地,在页面首次加载时节省编译时间。 1195 1196开发者需要创建一个无需渲染的离线Web组件,用于进行预编译,在预编译结束后使用其他Web组件加载对应的业务网页。 1197 1198注意事项: 1199 12001. 仅使用HTTP或HTTPS协议请求的JavaScript文件可以进行预编译操作。 12012. 不支持使用了ES6 Module的语法的JavaScript文件生成预编译字节码缓存。 12023. 通过配置参数中响应头中的E-Tag、Last-Modified对应的值标记JavaScript对应的缓存版本,对应的值发生变动则更新字节码缓存。 12034. 不支持本地JavaScript文件预编译缓存。 1204 1205**实践案例** 1206 1207【不推荐用法】 1208 1209在未使用预编译JavaScript前提下,启动加载Web页面。 1210 1211```typescript 1212import web_webview from '@ohos.web.webview'; 1213 1214@Entry 1215@Component 1216struct Index { 1217 controller: web_webview.WebviewController = new web_webview.WebviewController(); 1218 1219 build() { 1220 Column() { 1221 // 在适当的时机加载业务用Web组件,本例以Button点击触发为例。 1222 Button('加载页面') 1223 .onClick(() => { 1224 // url请替换为真实地址。 1225 this.controller.loadUrl('https://www.example.com/b.html'); 1226 }) 1227 Web({ src: 'https://www.example.com/a.html', controller: this.controller }) 1228 .fileAccess(true) 1229 .onPageBegin((event) => { 1230 console.info(`load page begin: ${event?.url}`); 1231 }) 1232 .onPageEnd((event) => { 1233 console.info(`load page end: ${event?.url}`); 1234 }) 1235 } 1236 } 1237} 1238``` 1239 1240点击“加载页面”按钮,性能打点数据如下,getMessageData进程中的Duration为加载页面开始到结束的耗时: 1241 1242 1243 1244 1245【推荐用法】 1246 1247使用预编译JavaScript生成字节码缓存,具体步骤如下: 1248 12491. 配置预编译的JavaScript文件信息。 1250 1251```typescript 1252import { webview } from '@kit.ArkWeb'; 1253 1254interface Config { 1255 url: string, 1256 localPath: string, // 本地资源路径。 1257 options: webview.CacheOptions 1258} 1259 1260@Entry 1261@Component 1262struct Index { 1263 // 配置预编译的JavaScript文件信息。 1264 configs: Array<Config> = [ 1265 { 1266 url: 'https://www/example.com/example.js', 1267 localPath: 'example.js', 1268 options: { 1269 responseHeaders: [ 1270 { headerKey: 'E-Tag', headerValue: 'aWO42N9P9dG/5xqYQCxsx+vDOoU=' }, 1271 { headerKey: 'Last-Modified', headerValue: 'Web, 21 Mar 2024 10:38:41 GMT' } 1272 ] 1273 } 1274 } 1275 ] 1276 // ... 1277} 1278``` 1279 12802. 读取配置,进行预编译。 1281 1282```typescript 1283Web({ src: 'https://www.example.com/a.html', controller: this.controller }) 1284 .onControllerAttached(async () => { 1285 // 读取配置,进行预编译。 1286 for (const config of this.configs) { 1287 let content = await (this.getUIContext() 1288 .getHostContext() as Context).resourceManager.getRawFileContentSync(config.localPath); 1289 1290 try { 1291 this.controller.precompileJavaScript(config.url, content, config.options) 1292 .then((errCode: number) => { 1293 console.info('precompile successfully!' ); 1294 }).catch((errCode: number) => { 1295 console.error('precompile failed.' + errCode); 1296 }) 1297 } catch (err) { 1298 console.error('precompile failed!.' + err.code + err.message); 1299 } 1300 } 1301 }) 1302``` 1303 1304 1305点击“加载页面”按钮,性能打点数据如下,getMessageData进程中的Duration为加载页面开始到结束的耗时: 1306 1307 1308 1309 1310> 说明 1311> 1312> 当需要更新本地已经生成的编译字节码时,修改cacheOptions参数中的responseHeaders中的E-Tag或Last-Modified响应头对应的值,再次调用接口即可。 1313 1314 1315 1316**总结** 1317 1318| **页面加载方式** | **耗时(局限不同设备和场景,数据仅供参考)** | **说明** | 1319| ------------------------------ | ------------------------------------------ | ------------------------------------------------------------ | 1320| 直接加载Web页面 | 3183ms | 在触发页面加载时才进行JavaScript编译,增加加载时间。 | 1321| 预编译JavaScript生成字节码缓存 | 268ms | 加载页面前完成预编译JavaScript,节省了跳转页面首次加载的编译时间。 | 1322 1323 1324 1325### 支持自定义协议的JavaScript生成字节码缓存(Code Cache) 1326 1327**原理介绍** 1328 1329支持自定义协议的JavaScript生成字节码缓存适用于在页面加载时存在自定义协议的JavaScript文件,支持其生成字节码缓存到本地,在页面非首次加载时节省编译时间。具体操作步骤如下: 1330 13311. 开发者首先需要在Web组件运行前,向Web组件注册自定义协议。 1332 13332. 其次需要拦截自定义协议的JavaScript,设置ResponseData和ResponseDataID,ResponseData为JavaScript内容,ResponseDataID用于区分JavaScript内容是否发生变更。若JavaScript内容变更,ResponseDataID需要一起变更。 1334 1335 1336**实践案例** 1337 1338**场景一 调用ArkTS接口, webview.WebviewController.customizeSchemes(schemes: Array\<WebCustomScheme>): void** 1339 1340【不推荐用法】 1341 1342直接加载包含自定义协议的JavaScript的Web页面。 1343 1344```typescript 1345// xxx.ets 1346import { webview } from '@kit.ArkWeb'; 1347import { BusinessError } from '@kit.BasicServicesKit'; 1348 1349@Entry 1350@Component 1351struct Index { 1352 controller: webview.WebviewController = new webview.WebviewController(); 1353 // 创建scheme对象,isCodeCacheSupported为false时不支持自定义协议的JavaScript生成字节码缓存。 1354 scheme: webview.WebCustomScheme = { schemeName: 'scheme', isSupportCORS: true, isSupportFetch: true, isCodeCacheSupported: false }; 1355 // 请求数据。 1356 @State jsData: string = 'xxx'; 1357 1358 aboutToAppear(): void { 1359 try { 1360 webview.WebviewController.customizeSchemes([this.scheme]); 1361 } catch (error) { 1362 const e: BusinessError = error as BusinessError; 1363 console.error(`ErrorCode: ${e.code}, Message: ${e.message}`); 1364 } 1365 } 1366 build() { 1367 Column({ space: 10 }) { 1368 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 1369 Web({ 1370 // 需将'https://www.example.com/'替换为真是的包含自定义协议的JavaScript的Web页面地址。 1371 src: 'https://www.example.com/', 1372 controller: this.controller 1373 }) 1374 .fileAccess(true) 1375 .javaScriptAccess(true) 1376 .onInterceptRequest(event => { 1377 const responseResource: WebResourceResponse = new WebResourceResponse(); 1378 // 拦截页面请求。 1379 if (event?.request.getRequestUrl() === 'scheme1://www.example.com/test.js') { 1380 responseResource.setResponseHeader([ 1381 { 1382 headerKey: 'ResponseDataId', 1383 // 格式:不超过13位的纯数字。JS识别码,JS有更新时必须更新该字段。 1384 headerValue: '0000000000001' 1385 } 1386 ]); 1387 responseResource.setResponseData(this.jsData); 1388 responseResource.setResponseEncoding('utf-8'); 1389 responseResource.setResponseMimeType('application/javascript'); 1390 responseResource.setResponseCode(200); 1391 responseResource.setReasonMessage('OK'); 1392 return responseResource; 1393 } 1394 return null; 1395 }) 1396 } 1397 } 1398 } 1399} 1400``` 1401 1402性能打点数据如下,getMessageData进程中的Duration为加载页面开始到结束的耗时: 1403 1404 1405 1406 1407【推荐用法】 1408 1409支持自定义协议JS生成字节码缓存,具体步骤如下: 1410 14111. 将scheme对象属性isCodeCacheSupported设置为true,支持自定义协议的JavaScript生成字节码缓存。 1412 1413```typescript 1414scheme: webview.WebCustomScheme = { schemeName: 'scheme', isSupportCORS: true, isSupportFetch: true, isCodeCacheSupported: true }; 1415``` 1416 14172. 在Web组件运行前,向Web组件注册自定义协议。 1418 1419> 说明 1420> 不得与Web内核内置协议相同。 1421 1422```typescript 1423// xxx.ets 1424aboutToAppear(): void { 1425 try { 1426 webview.WebviewController.customizeSchemes([this.scheme]); 1427 } catch (error) { 1428 const e: BusinessError = error as BusinessError; 1429 console.error(`ErrorCode: ${e.code}, Message: ${e.message}`); 1430 } 1431} 1432``` 1433 14343. 拦截自定义协议的JavaScript,设置ResponseData和ResponseDataID。ResponseData为JS内容,ResponseDataID用于区分JS内容是否发生变更。 1435 1436> 说明 1437> 若JS内容变更,ResponseDataID需要一起变更。 1438 1439```typescript 1440// xxx.ets 1441Web({ 1442 // 需将'https://www.example.com/'替换为真是的包含自定义协议的JavaScript的Web页面地址。 1443 src: 'https://www.example.com/', 1444 controller: this.controller 1445}) 1446 .fileAccess(true) 1447 .javaScriptAccess(true) 1448 .onInterceptRequest(event => { 1449 const responseResource: WebResourceResponse = new WebResourceResponse(); 1450 // 拦截页面请求。 1451 if (event?.request.getRequestUrl() === 'scheme1://www.example.com/test.js') { 1452 responseResource.setResponseHeader([ 1453 { 1454 headerKey: 'ResponseDataId', 1455 // 格式:不超过13位的纯数字。JS识别码,JS有更新时必须更新该字段。 1456 headerValue: '0000000000001' 1457 } 1458 ]); 1459 responseResource.setResponseData(this.jsData2); 1460 responseResource.setResponseEncoding('utf-8'); 1461 responseResource.setResponseMimeType('application/javascript'); 1462 responseResource.setResponseCode(200); 1463 responseResource.setReasonMessage('OK'); 1464 return responseResource; 1465 } 1466 return null; 1467 }) 1468``` 1469 1470性能打点数据如下,getMessageData进程中的Duration为加载页面开始到结束的耗时: 1471 1472 1473 1474 1475**场景二 调用Native接口,int32_t OH_ArkWeb_RegisterCustomSchemes(const char * scheme, int32_t option)** 1476 1477【不推荐用法】 1478 1479通过网络拦截接口对Web组件发出的请求进行拦截,Demo工程构建请参考[拦截Web组件发起的网络请求](../web/web-scheme-handler.md)。 1480 1481 1482性能打点数据如下,getMessageData进程中的Avg Wall Duration为两次加载页面开始到结束的平均耗时: 1483 1484 1485 1486 1487【推荐用法】 1488 1489支持将自定义协议的JavaScript资源生成code cache,具体步骤如下: 1490 14911. 注册三方协议配置时,传入ARKWEB_SCHEME_OPTION_CODE_CACHE_ENABLED参数。 1492 1493```c 1494// 注册三方协议的配置,需要在Web内核初始化之前调用,否则会注册失败。 1495static napi_value RegisterCustomSchemes(napi_env env, napi_callback_info info) 1496{ 1497 OH_LOG_INFO(LOG_APP, "register custom schemes"); 1498 OH_ArkWeb_RegisterCustomSchemes("custom", ARKWEB_SCHEME_OPTION_STANDARD | ARKWEB_SCHEME_OPTION_CORS_ENABLED | ARKWEB_SCHEME_OPTION_CODE_CACHE_ENABLED); 1499 return nullptr; 1500} 1501``` 1502 15032. 设置ResponsesDataId。 1504 1505```c 1506// 在worker线程中读取rawfile,并通过ResourceHandler返回给Web内核。 1507void RawfileRequest::ReadRawfileDataOnWorkerThread() { 1508 // ... 1509 if ('test-cc.js' == rawfilePath()) { 1510 OH_ArkWebResponse_SetHeaderByName(response(), "ResponseDataID", "0000000000001", true); 1511 } 1512 OH_ArkWebResponse_SetCharset(response(), "UTF-8"); 1513} 1514``` 1515 15163. 注册三方协议的配置,设置SchemeHandler。 1517 1518```typescript 1519// EntryAbility.ets 1520import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit'; 1521import { webview } from '@kit.ArkWeb'; 1522import { window } from '@kit.ArkUI'; 1523import testNapi from 'libentry.so'; 1524 1525export default class EntryAbility extends UIAbility { 1526 onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 1527 // 注册三方协议的配置。 1528 testNapi.registerCustomSchemes(); 1529 // 初始化Web组件内核,该操作会初始化Brownser进程以及创建BrownserContext。 1530 webview.WebviewController.initializeWebEngine(); 1531 // 设置SchemeHandler。 1532 testNapi.setSchemeHandler(); 1533 } 1534 // ... 1535} 1536``` 1537 1538 1539性能打点数据如下,getMessageData进程中的Avg Wall Duration为两次加载页面开始到结束的平均耗时: 1540 1541 1542 1543 1544 1545**总结(以Native接口性能数据举例)** 1546 1547| **页面加载方式** | **耗时(局限不同设备和场景,数据仅供参考)** | **说明** | 1548| ------------------------------------ | ------------------------------------------ | ------------------------------------------------------------ | 1549| 直接加载Web页面 | 8ms | 在触发页面加载时才进行JavaScript编译,增加加载时间。 | 1550| 自定义协议的JavaScript生成字节码缓存 | 4ms | 支持自定义协议头的JS文件在第二次加载JS时生成code cache,节约了第三次及之后的页面加载或跳转的自定义协议JS文件的编译时间,提升了页面加载和跳转的性能。 | 1551 1552 1553 1554### 离线资源免拦截注入 1555 1556**原理介绍** 1557 1558离线资源免拦截注入适用于在页面加载之前提前将即将使用到的图片、样式表和脚本资源注入到内存缓存中,在页面首次加载时节省网络请求时间。 1559 1560注意事项: 1561 15621. 开发者需创建一个无需渲染的离线Web组件,用于将资源注入到内存缓存中,使用其他Web组件加载对应的业务网页。 15632. 仅使用HTTP或HTTPS协议请求的资源可被注入进内存缓存。 15643. 内存缓存中的资源由内核自动管理,当注入的资源过多导致内存压力过大,内核自动释放未使用的资源,应避免注入大量资源到内存缓存中。 15654. 正常情况下,资源的有效期由提供的Cache-Control或Expires响应头控制其有效期,默认的有效期为86400秒,即1天。 15665. 资源的MIMEType通过提供的参数中的Content-Type响应头配置,Content-Type需符合标准,否则无法正常使用,MODULE_JS必须提供有效的MIMEType,其他类型可不提供。 15676. 仅支持通过HTML中的标签加载。 15687. 如果业务网页中的script标签使用了crossorigin属性,则必须在接口的responseHeaders参数中设置Cross-Origin响应头的值为anonymous或use-credentials。 15698. 当调用web_webview.WebviewController.SetRenderProcessMode(web_webview.RenderProcessMode.MULTIPLE)接口后,应用会启动多渲染进程模式,此方案在此场景下不会生效。 15709. 单次调用最大支持注入30个资源,单个资源最大支持10Mb。 1571 1572 1573**实践案例** 1574 1575【不推荐用法】 1576 1577直接加载Web页面。 1578 1579```typescript 1580import webview from '@ohos.web.webview'; 1581 1582@Entry 1583@Component 1584struct Index { 1585 controller: webview.WebviewController = new webview.WebviewController(); 1586 1587 build() { 1588 Column() { 1589 // 在适当的时机加载业务用Web组件,本例以Button点击触发为例。 1590 Button('加载页面') 1591 .onClick(() => { 1592 this.controller.loadUrl('https://www.example.com/b.html'); 1593 }) 1594 Web({ src: 'https://www.example.com/a.html', controller: this.controller }) 1595 .fileAccess(true) 1596 } 1597 } 1598} 1599``` 1600 1601性能打点数据如下,getMessageData进程中的Duration为加载页面开始到结束的耗时: 1602 1603 1604 1605 1606【推荐用法】 1607 1608使用资源免拦截注入加载Web页面,请参考以下步骤: 1609 16101. 创建资源配置。 1611 1612```typescript 1613interface ResourceConfig { 1614 urlList: Array<string>; 1615 type: webview.OfflineResourceType; 1616 responseHeaders: Array<Header>; 1617 localPath: string; // 本地资源存放在rawfile目录下的路径。 1618} 1619 1620const configs: Array<ResourceConfig> = [ 1621 { 1622 localPath: 'example.png', 1623 urlList: [ 1624 // 多url场景,第一个url作为资源的源。 1625 'https://www.example.com/', 1626 'https://www.example.com/path1/example.png', 1627 'https://www.example.com/path2/example.png' 1628 ], 1629 type: webview.OfflineResourceType.IMAGE, 1630 responseHeaders: [ 1631 { headerKey: 'Cache-Control', headerValue: 'max-age=1000' }, 1632 { headerKey: 'Content-Type', headerValue: 'image/png' } 1633 ] 1634 }, 1635 { 1636 localPath: 'example.js', 1637 urlList: [ 1638 // 仅提供一个url,这个url既作为资源的源,也作为资源的网络请求地址。 1639 'https://www.example.com/example.js' 1640 ], 1641 type: webview.OfflineResourceType.CLASSIC_JS, 1642 responseHeaders: [ 1643 // 以<script crossorigin='anonymous'/>方式使用,提供额外的响应头。 1644 { headerKey: 'Cross-Origin', headerValue: 'anonymous' } 1645 ] 1646 } 1647]; 1648 1649``` 1650 16512. 读取配置,注入资源。 1652 1653```typescript 1654Web({ src: 'https://www.example.com/a.html', controller: this.controller }) 1655 .onControllerAttached(async () => { 1656 try { 1657 const resourceMapArr: Array<webview.OfflineResourceMap> = []; 1658 // 读取配置,从rawfile目录中读取文件内容。 1659 for (const config of this.configs) { 1660 const buf: Uint8Array = await (this.getUIContext() 1661 .getHostContext() as Context).resourceManager.getRawFileContentSync(config.localPath); 1662 resourceMapArr.push({ 1663 urlList: config.urlList, 1664 resource: buf, 1665 responseHeaders: config.responseHeaders, 1666 type: config.type 1667 }); 1668 } 1669 // 注入资源。 1670 this.controller.injectOfflineResources(resourceMapArr); 1671 } catch (err) { 1672 console.error('error: ' + err.code + ' ' + err.message); 1673 } 1674 }) 1675``` 1676 1677性能打点数据如下,getMessageData进程中的Duration为加载页面开始到结束的耗时: 1678 1679 1680 1681**总结** 1682 1683| **页面加载方式** | **耗时(局限不同设备和场景,数据仅供参考)** | **说明** | 1684| --------------------------------- | ------------------------------------------ | -------------------------------------------------- | 1685| 直接加载Web页面 | 1312ms | 在触发页面加载时才发起资源请求,增加页面加载时间。 | 1686| 使用离线资源免拦截注入加载Web页面 | 74ms | 将资源预置在内存中,节省了网络请求时间。 | 1687 1688 1689 1690### 资源拦截替换加速 1691 1692**原理介绍** 1693 1694资源拦截替换加速在原本的资源拦截替换接口基础上新增支持了ArrayBuffer格式的入参,开发者无需在应用侧进行ArrayBuffer到String格式的转换,可直接使用ArrayBuffer格式的数据进行拦截替换。 1695 1696> 说明 1697> 1698> 本方案与原本的资源拦截替换接口在使用上没有任何区别,开发者仅需在调用WebResourceResponse.setResponseData()接口时传入ArrayBuffer格式的数据即可。 1699 1700 1701**实践案例** 1702 1703【不推荐用法】 1704 1705使用字符串格式的数据做拦截替换。 1706 1707```typescript 1708import webview from '@ohos.web.webview'; 1709 1710@Entry 1711@Component 1712struct Index { 1713 controller: webview.WebviewController = new webview.WebviewController(); 1714 responseResource: WebResourceResponse = new WebResourceResponse(); 1715 // 这里是string格式数据。 1716 resourceStr: string = 'xxxxxxxxxxxxxxx'; 1717 1718 build() { 1719 Column() { 1720 Web({ src: 'https:www.example.com/test.html', controller: this.controller }) 1721 .onInterceptRequest(event => { 1722 if (event) { 1723 if (!event.request.getRequestUrl().startsWith('https://www.example.com/')) { 1724 return null; 1725 } 1726 } 1727 // 使用string格式的数据做拦截替换。 1728 this.responseResource.setResponseData(this.resourceStr); 1729 this.responseResource.setResponseEncoding('utf-8'); 1730 this.responseResource.setResponseMimeType('text/json'); 1731 this.responseResource.setResponseCode(200); 1732 this.responseResource.setReasonMessage('OK'); 1733 this.responseResource.setResponseHeader([{ headerKey: 'Access-Control-Allow-Origin', headerValue: '*' }]); 1734 return this.responseResource; 1735 }) 1736 } 1737 } 1738} 1739``` 1740 1741资源替换耗时如图所示,getMessageData ... someFunction took后的时间页面加载资源的耗时: 1742 1743 1744 1745 1746【推荐用法】 1747 1748使用ArrayBuffer格式的数据做拦截替换。 1749 1750```typescript 1751import webview from '@ohos.web.webview'; 1752 1753@Entry 1754@Component 1755struct Index { 1756 controller: webview.WebviewController = new webview.WebviewController(); 1757 responseResource: WebResourceResponse = new WebResourceResponse(); 1758 // 这里是ArrayBuffer格式数据。 1759 buffer: ArrayBuffer = new ArrayBuffer(10); 1760 1761 build() { 1762 Column() { 1763 Web({ src: 'https:www.example.com/test.html', controller: this.controller }) 1764 .onInterceptRequest(event => { 1765 if (event) { 1766 if (!event.request.getRequestUrl().startsWith('https://www.example.com/')) { 1767 return null; 1768 } 1769 } 1770 // 使用ArrayBuffer格式的数据做拦截替换。 1771 this.responseResource.setResponseData(this.buffer); 1772 this.responseResource.setResponseEncoding('utf-8'); 1773 this.responseResource.setResponseMimeType('text/json'); 1774 this.responseResource.setResponseCode(200); 1775 this.responseResource.setReasonMessage('OK'); 1776 this.responseResource.setResponseHeader([{ headerKey: 'Access-Control-Allow-Origin', headerValue: '*' }]); 1777 return this.responseResource; 1778 }) 1779 } 1780 } 1781} 1782``` 1783 1784资源替换耗时如图所示,getMessageData william someFunction took后的时间页面加载资源的耗时: 1785 1786 1787 1788 1789 1790**总结** 1791 1792 1793| **页面加载方式** | **耗时(局限不同设备和场景,数据仅供参考)** | **说明** | 1794| ----------------------------------- | ------------------------------------------ | ------------------------------------------------------------ | 1795| 使用string格式的数据做拦截替换 | 34ms | Web组件内部数据传输仍需要转换为ArrayBuffer,增加数据处理步骤,增加启动耗时。 | 1796| 使用ArrayBuffer格式的数据做拦截替换 | 13ms | 接口直接支持ArrayBuffer格式,节省了转换时间,同时对ArrayBuffer格式的数据传输方式进行了优化,进一步减少耗时。 | 1797 1798### 预加载优化滑动白块 1799 1800Web场景应用在加载图片资源时,需要先发起请求,然后解析渲染到屏幕上。在列表滑动过程中,如果等屏幕可视区域出现新图片时才开始发起请求,会因上述加载资源的步骤出现时间差,导致列表中图片出现白块问题,在网络情况不良或应用渲染图片阻塞时,这种情况会更加严重。本章节针对Web场景,在HTML页面中使用预加载策略,使列表滑动前预先加载可视区域外的图片资源,解决可视区域白块问题,提高用户使用体验。 1801 1802**原理介绍** 1803 1804滑动白块的产生主要来源于页面滑动场景组件可见和组件上屏刷新之间的时间差,在这两个时间点间,由于网络图片未加载完成,该区域显示的是默认图片即图片白块。图片组件从可见到上屏刷新之间的耗时主要是由图片资源网络请求和解码渲染两部分组成,在这段时间内页面滑动距离是滑动速度(px/ms)*(下载耗时+解码耗时)(ms),因此只要设置预加载的高度大于滑动距离,就可以保证页面基本无白块。开发者可根据`预加载高度(px)>滑动速度(px/ms)*(下载耗时+解码耗时)(ms)`这一计算公式对应用进行调整,计算出Web页面在设备视窗外需要预加载的图片个数,即可视窗口根元素超过屏幕的高度。 1805 1806开发者可以使用IntersectionObserver接口,将视窗作为根元素并对其进行观察,当图片滑动进入视窗时替换默认地址为真实地址,触发图片加载。此时适当的扩展视窗高度,就可以实现在图片进入视窗前提前开始加载图片,解决图片未及时加载导致出现白块的问题。 1807 1808**实践案例** 1809 1810【不推荐用法】 1811 1812常规案例使用懒加载的逻辑加载图片,图片组件进入可视区域后再执行加载,滑动过程中列表有大量图片未加载完成产生的白块。 1813 1814 1815 1816```html 1817<!DOCTYPE html> 1818<html> 1819 <head> 1820 <title>Image List</title> 1821 </head> 1822 <body> 1823 <ul> 1824 <li><img src="default.jpg" data-src="photo1.jpg" alt="Photo 1"></li> 1825 <li><img src="default.jpg" data-src="photo2.jpg" alt="Photo 2"></li> 1826 <li><img src="default.jpg" data-src="photo3.jpg" alt="Photo 3"></li> 1827 <li><img src="default.jpg" data-src="photo4.jpg" alt="Photo 4"></li> 1828 <li><img src="default.jpg" data-src="photo5.jpg" alt="Photo 5"></li> 1829 <!-- 添加更多的图片只需要复制并修改src和alt属性即可 --> 1830 </ul> 1831 </body> 1832 <script> 1833 window.onload = function(){ 1834 // 可视窗口作为根元素,不进行扩展。 1835 const options = {root:document,rootMargin:'0% 0% 0% 0%'} 1836 // 创建一个IntersectionObserver实例。 1837 const observer = new IntersectionObserver(function(entries,observer){ 1838 entries.forEach(function(entry){ 1839 // 检查图片是否进入可视区域。 1840 if(entry.isIntersecting){ 1841 const image = entry.target; 1842 // 将数据源的src赋值给img的src。 1843 image.src = image.dataset.src; 1844 // 停止观察该图片。 1845 observer.unobserve(image); 1846 } 1847 }) 1848 },options); 1849 1850 document.querySelectorAll('img').forEach(img => { observer.observe(img) }); 1851 } 1852 </script> 1853</html> 1854``` 1855 1856【推荐用法】 1857 1858根据上方公式,优化案例设定在400mm/s的速度滑动屏幕,此时可计算应用需预加载0.5个屏幕高度的图片。在常规加载案例中,页面将可视窗口作为根元素,rootMargin属性均为0,可视窗口与设备屏幕高度相等。此时可通过设置`rootMargin`向下方向为50%(即0.5个屏幕高度),扩展可视窗口的高度,使图片在屏幕外提前进入可视窗口。当图片元素进入可视窗口时,会将img标签的data-src属性中保存的图片地址赋值给src属性,从而实现图片的预加载。应用会查询页面上所有具有data-src属性的img标签,并开始观察这些图片。当某张图片进入已拓展高度的可视窗口时,就会执行相应的加载操作,实现页面预渲染更多图片,解决滑动白块问题。 1859 1860```javascript 1861// html结构与上方常规案例相同。 1862// 可视区域作为根元素,向下扩展50%的margin长度。 1863const options = {root:document,rootMargin:'0% 0% 50% 0%'}; 1864// 创建IntersectionObserver实例。 1865const observer = new IntersectionObserver(function(entries,observer){ 1866 // ... 1867},options); 1868 1869document.querySelectorAll('img').forEach(img => {observer.observe(img)}); 1870``` 1871 1872 1873 1874**总结** 1875 1876| 图片加载方式 | 说明 | 1877| ---------------------- | ------------------------------------------------------------ | 1878| 常规加载(不推荐用法) | 常规案例在列表滑动过程中,由于图片加载未及时导致出现大量白块,影响用户体验。 | 1879| 预加载(推荐用法) | 优化案例在拓展0.5个屏幕高度的可视窗口后,滑动时无明显白块,用户体验提升。 | 1880 1881开发者可使用公式,根据设备屏幕高度和设置滑动屏幕速度预估值,计算出视窗根元素需要扩展的高度,解决滑动白块问题。 1882 1883 1884## 性能分析 1885 1886### 场景示例 1887 1888构建通过点击按钮跳转Web网页和在网页内跳转页面的场景,在点击按钮触发跳转事件、Web组件触发OnPageEnd事件处使用Hilog打点记录时间戳。 1889 1890**反例** 1891 1892入口页通过router实现跳转。 1893```javascript 1894// src/main/ets/pages/WebUninitialized.ets 1895 1896Button('进入网页') 1897 .onClick(() => { 1898 hilog.info(0x0001, "WebPerformance", "UnInitializedWeb"); 1899 this.getUIContext().getRouter().pushUrl({ url: 'pages/WebBrowser' }); 1900 }) 1901``` 1902Web页使用Web组件加载指定网页。 1903```javascript 1904// src/main/ets/pages/WebBrowser.ets 1905 1906Web({ src: 'https://www.example.com', controller: this.controller }) 1907 .domStorageAccess(true) 1908 .onPageEnd((event) => { 1909 if (event) { 1910 hilog.info(0x0001, "WebPerformance", "WebPageOpenEnd"); 1911 } 1912 }) 1913``` 1914 1915**正例** 1916 1917入口页提前进行Web组件的初始化和预连接。 1918 1919```typescript 1920// src/main/ets/pages/WebInitialized.ets 1921 1922import { webview } from '@kit.ArkWeb'; 1923import { router } from '@kit.ArkUI'; 1924import { hilog } from '@kit.PerformanceAnalysisKit'; 1925 1926@Entry 1927@Component 1928struct WebComponent { 1929 controller: webview.WebviewController = new webview.WebviewController(); 1930 1931 aboutToAppear() { 1932 webview.WebviewController.initializeWebEngine(); 1933 webview.WebviewController.prepareForPageLoad("https://www.example.com", true, 2); 1934 } 1935 1936 build() { 1937 Column() { 1938 Button('进入网页') 1939 .onClick(() => { 1940 hilog.info(0x0001, "WebPerformance", "InitializedWeb"); 1941 this.getUIContext().getRouter().pushUrl({ url: 'pages/WebBrowser' }); 1942 }) 1943 } 1944 } 1945} 1946``` 1947 1948Web页加载的同时使用prefetchPage预加载下一页。 1949 1950```typescript 1951// src/main/ets/pages/WebBrowser.ets 1952 1953import { webview } from '@kit.ArkWeb'; 1954import { hilog } from '@kit.PerformanceAnalysisKit'; 1955 1956@Entry 1957@Component 1958struct WebComponent { 1959 controller: webview.WebviewController = new webview.WebviewController(); 1960 1961 build() { 1962 Column() { 1963 // ... 1964 Web({ src: 'https://www.example.com', controller: this.controller }) 1965 .domStorageAccess(true) 1966 .onPageEnd((event) => { 1967 if (event) { 1968 hilog.info(0x0001, "WebPerformance", "WebPageOpenEnd"); 1969 this.controller.prefetchPage('https://www.example.com/nextpage'); 1970 } 1971 }) 1972 } 1973 } 1974} 1975``` 1976 1977### 数据对比 1978 1979通过分别抓取正反示例的trace数据后使用SmartPerf Host工具分析可以得出以下结论: 1980 1981 1982 1983从点击按钮进入Web首页到Web组件触发OnPageEnd事件,表示首页加载完成。对比优化前后时延可以得出,使用提前初始化内核和预解析、预连接可以减少平均100ms左右的加载时间。 1984 1985 1986 1987从Web首页内点击跳转下一页按钮到Web组件触发OnPageEnd事件,表示页面间跳转完成。对比优化前后时延可以得出,使用预加载下一页方法可以减少平均40~50ms左右的跳转时间。 1988 1989 1990 1991