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