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![Web组件显示页面的阶段](./figures/web-display-stage.png)
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![Web组件的生命周期回调函数](./figures/web-life-cycle.png)
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![](figures/web_enginee_un_init.png)
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![](figures/web_enginee_init.png)
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![](./figures/web-node-container.png)
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![img.png](figures/web_jsbridge_ets_ndk_compare.png)
576
577上图为具有普适性的小程序一般架构,其中逻辑层需要应用自带JavaScript运行时,本身已存在C++环境,通过native接口可直接在C++环境中完成与视图层(ArkWeb作为渲染器)的通信,无需再返回ArkTS环境调用JSBridge相关接口。
578![img.png](figures/web_jsbridge_ets_ndk_compare_new.png)
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![img.png](figures/web_jsbridge_h5_screen.png)
651
652![img.png](figures/web_jsbridge_ets_screen.png)
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.hentry/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![img.png](figures/web_jsbridge_ndk_ets_screen.png)
865
866![img.png](figures/web_jsbridge_ndk_h5_screen.png)
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![img.png](figures/web_jsbridge_async_compare.png)
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![](figures/web_js_un_pre_compile.png)
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![](figures/web_js_pre_compile.png)
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![](figures/web_schemes_un_customize.png)
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![](figures/web_schemes_customize.png)
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![](figures/web_schemes_un_registe.png)
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![](figures/web_schemes_registe.png)
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![](figures/web_resource_un_offline.png)
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![](figures/web_resource_offline.png)
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![](figures/web_send_response_data_string.png)
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![](figures/web_send_response_data_buffer.png)
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![img](figures/web-sliding-white-block-optimization-1.gif)
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![img](figures/web-sliding-white-block-optimization-2.gif)
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![hilog](./figures/web-hilog.png)
1973
1974从点击按钮进入Web首页到Web组件触发OnPageEnd事件,表示首页加载完成。对比优化前后时延可以得出,使用提前初始化内核和预解析、预连接可以减少平均100ms左右的加载时间。
1975
1976![首页完成时延](./figures/web-open-time-chart.png)
1977
1978从Web首页内点击跳转下一页按钮到Web组件触发OnPageEnd事件,表示页面间跳转完成。对比优化前后时延可以得出,使用预加载下一页方法可以减少平均40~50ms左右的跳转时间。
1979
1980![跳转完成时延](./figures/web-route-time-chart.png)
1981
1982