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