鸿蒙开发5.0案例分析:Web场景性能优化(二)
📝往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)
🚩 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
🚩 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
🚩 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
🚩 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
🚩 记录一场鸿蒙开发岗位面试经历~
📃 持续更新中……
JSBridge
JSBridge优化解决方案
适用场景
应用使用ArkTS、C++语言混合开发,或本身应用架构较贴近于小程序架构,自带C++侧环境, 推荐使用ArkWeb在Native侧提供的ArkWeb_ControllerAPI、ArkWeb_ComponentAPI实现JSBridge功能。
上图为具有普适性的小程序一般架构,其中逻辑层需要应用自带JavaScript运行时,本身已存在C++环境,通过Native接口可直接在C++环境中完成与视图层(ArkWeb作为渲染器)的通信,无需再返回ArkTS环境调用JSBridge相关接口。
Native JSBridge方案可以解决ArkTS环境的冗余切换,同时允许回调在非UI线程上报,避免造成UI阻塞。
实践案例
案例一:使用ArkTS接口实现JSBridge通信。
应用侧代码:
import { webview } from '@kit.ArkWeb'; @Entry @Component struct WebComponent { webviewController: webview.WebviewController = new webview.WebviewController(); aboutToAppear() { // 配置Web开启调试模式 webview.WebviewController.setWebDebuggingAccess(true); } build() { Column() { Button('runJavaScript') .onClick(() => { console.info('现在时间是:' + new Date().getTime()); // 前端页面函数无参时,将param删除。 this.webviewController.runJavaScript('htmlTest(param)'); }) Button('runJavaScriptCodePassed') .onClick(() => { // 传递runJavaScript侧代码方法。 this.webviewController.runJavaScript(`function changeColor(){document.getElementById('text').style.color = 'red'}`); }) Web({ src: $rawfile('index.html'), controller: this.webviewController }) } } }
前端页面代码:
Click Me!这是一个测试信息,默认字体为黑色,调用runJavaScript方法后字体为绿色,调用runJavaScriptCodePassed方法后字体为红色
// 调用有参函数时实现。 var param = "param: JavaScript Hello World!"; function htmlTest(param) { document.getElementById('text').style.color = 'green'; document.getElementById('text').innerHTML = '现在时间:'+new Date().getTime() console.log(param); } // 调用无参函数时实现。 function htmlTest() { document.getElementById('text').style.color = 'green'; document.getElementById('text').innerHTML = '现在时间:'+new Date().getTime(); } // Click Me!触发前端页面callArkTS()函数执行JavaScript传递的代码。 function callArkTS() { changeColor(); }
点击runJavaScript按钮后触发h5页面htmlTest方法,使得页面内容变更为当前时间戳,如下图所示:
经过多轮测试,可以得出从点击原生button到h5触发htmlTest方法,耗时约7ms~9ms。
案例二:使用NDK接口实现JSBridge通信。
应用侧代码:
import testNapi from 'libentry.so'; import { webview } from '@kit.ArkWeb'; class testObj { constructor() { } test(): string { console.log('ArkUI Web Component'); return "ArkUI Web Component"; } toString(): void { console.log('Web Component toString'); } } @Entry @Component struct Index { webTag: string = 'ArkWeb1'; controller: webview.WebviewController = new webview.WebviewController(this.webTag); @State testObjtest: testObj = new testObj(); aboutToAppear() { console.info("aboutToAppear"); //初始化web ndk testNapi.nativeWebInit(this.webTag); } build() { Column() { Row() { Button('runJS hello') .fontSize(12) .onClick(() => { console.log('start:---->'+new Date().getTime()); testNapi.runJavaScript(this.webTag, "runJSRetStr(\"" + "hello" + "\")"); }) }.height('20%') Row() { Web({ src: $rawfile('runJS.html'), controller: this.controller }) .javaScriptAccess(true) .fileAccess(true) .onControllerAttached(() => { console.error("ndk onControllerAttached webId: " + this.controller.getWebId()); }) }.height('80%') } } }
hello.cpp作为应用C++侧业务逻辑代码:
// 注册对象及方法,发送脚本到H5执行后的回调,解析存储应用侧传过来的实例等代码逻辑这里不进行展示,开发者根据自身业务场景自行实现。 // 发送JS脚本到H5侧执行 static napi_value RunJavaScript(napi_env env, napi_callback_info info) { size_t argc = 2; napi_value args[2] = {nullptr}; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); // 获取第一个参数 webTag size_t webTagSize = 0; napi_get_value_string_utf8(env, args[0], nullptr, 0, &webTagSize); char *webTagValue = new (std::nothrow) char[webTagSize + 1]; size_t webTagLength = 0; napi_get_value_string_utf8(env, args[0], webTagValue, webTagSize + 1, &webTagLength); OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", "ndk OH_NativeArkWeb_RunJavaScript webTag:%{public}s", webTagValue); // 获取第二个参数 jsCode size_t bufferSize = 0; napi_get_value_string_utf8(env, args[1], nullptr, 0, &bufferSize); char *jsCode = new (std::nothrow) char[bufferSize + 1]; size_t byteLength = 0; napi_get_value_string_utf8(env, args[1], jsCode, bufferSize + 1, &byteLength); OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", "ndk OH_NativeArkWeb_RunJavaScript jsCode len:%{public}zu", strlen(jsCode)); // 构造runJS执行的结构体 ArkWeb_JavaScriptObject object = {(uint8_t *)jsCode, bufferSize, &JSBridgeObject::StaticRunJavaScriptCallback, static_cast(jsbridge_object_ptr->GetWeakPtr())}; controller->runJavaScript(webTagValue, &object); return nullptr; } EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { {"nativeWebInit", nullptr, NativeWebInit, nullptr, nullptr, nullptr, napi_default, nullptr}, {"runJavaScript", nullptr, RunJavaScript, nullptr, nullptr, nullptr, napi_default, nullptr} }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } EXTERN_C_END static napi_module demoModule = { .nm_version = 1, .nm_flags = 0, .nm_filename = nullptr, .nm_register_func = Init, .nm_modname = "entry", .nm_priv = ((void *)0), .reserved = {0} }; extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(&demoModule); }
Native侧业务代码entry/src/main/cpp/jsbridge_object.h、entry/src/main/cpp/jsbridge_object.cpp详见 应用侧与前端页面的相互调用(C/C++) 。
runJS.html作为应用前端页面:
run javascript demorun JavaScript Ext demo
test ndk method1 !
test ndk method2 !
function testNdkProxyObjMethod1() { //校验ndk方法是否已经注册到window if (window.ndkProxy == undefined) { document.getElementById("webDemo").innerHTML = "ndkProxy undefined"; return "objName undefined"; } if (window.ndkProxy.method1 == undefined) { document.getElementById("webDemo").innerHTML = "ndkProxy method1 undefined"; return "objName test undefined"; } if (window.ndkProxy.method2 == undefined) { document.getElementById("webDemo").innerHTML = "ndkProxy method2 undefined"; return "objName test undefined"; } //调用ndk注册到window的method1方法,并将结果回显到p标签 var retStr = window.ndkProxy.method1("hello", "world", [1.2, -3.4, 123.456], ["Saab", "Volvo", "BMW", undefined], 1.23456, 123789, true, false, 0, undefined); document.getElementById("webDemo").innerHTML = "ndkProxy and method1 is ok, " + retStr; } function testNdkProxyObjMethod2() { //校验ndk方法是否已经注册到window if (window.ndkProxy == undefined) { document.getElementById("webDemo").innerHTML = "ndkProxy undefined"; return "objName undefined"; } if (window.ndkProxy.method1 == undefined) { document.getElementById("webDemo").innerHTML = "ndkProxy method1 undefined"; return "objName test undefined"; } if (window.ndkProxy.method2 == undefined) { document.getElementById("webDemo").innerHTML = "ndkProxy method2 undefined"; return "objName test undefined"; } var student = { name:"zhang", sex:"man", age:25 }; var cars = [student, 456, false, 4.567]; let params = "[\"{\\\"scope\\\"]"; //调用ndk注册到window的method2方法,并将结果回显到p标签 var retStr = window.ndkProxy.method2("hello", "world", false, cars, params); document.getElementById("webDemo").innerHTML = "ndkProxy and method2 is ok, " + retStr; } function runJSRetStr(data) { const d = new Date(); let time = d.getTime(); document.getElementById("webDemo").innerHTML = new Date().getTime(); return JSON.stringify(time); }
点击runJS hello按钮后触发h5页面runJSRetStr方法,使得页面内容变更为当前时间戳。
经过多轮测试,可以得出从点击原生button到h5触发runJSRetStr方法,耗时约2ms~6ms。
总结
通信方式 | 耗时(局限不同设备和场景,数据仅供参考) | 说明 |
---|---|---|
ArkWeb实现与前端页面通信 | 7ms~9ms | ArkTS环境冗余切换,耗时较长 |
ArkWeb、c++实现与前端页面通信 | 2ms~6ms | 避免ArkTS环境冗余切换,耗时短 |
JSBridge优化方案适用于ArkWeb应用侧与前端网页通信场景,开发者可根据应用架构选择合适的业务通信机制:
- 应用使用ArkTS语言开发,推荐使用ArkWeb在ArkTS提供的runJavaScriptExt接口实现应用侧至前端页面的通信,同时使用registerJavaScriptProxy实现前端页面至应用侧的通信。
- 应用使用ArkTS、C++语言混合开发,或本身应用结构较贴近于小程序架构,自带C++侧环境,推荐使用ArkWeb在NDK侧提供的OH_NativeArkWeb_RunJavaScript及OH_NativeArkWeb_RegisterJavaScriptProxy接口实现JSBridge功能。
说明
开发者需根据当前业务区分是否存在C++侧环境(较为显著标志点为当前应用是否使用了Node API技术进行开发,若是则该应用具备C++侧环境)。 具备C++侧环境的应用开发,可使用ArkWeb提供的NDK侧JSBridge接口。 不具备C++侧环境的应用开发,可使用ArkWeb侧JSBridge接口。
异步JSBridge调用
原理介绍
异步JSBridge调用适用于H5侧调用原生或C++侧注册的JSBridge函数场景下,将用户指定的JSBridge接口的调用抛出后,不等待执行结果, 以避免在ArkUI主线程负载重时JSBridge同步调用可能导致Web线程等待IPC时间过长,从而造成阻塞的问题。
实践案例
案例一:使用ArkTS接口实现JSBridge通信,具体步骤如下:
- 只注册同步函数
import { webview } from '@kit.ArkWeb'; // 定义ETS侧对象及函数 class TestObj { constructor() {} test(testStr:string): string { let start = Date.now(); // 模拟耗时操作 for(let i = 0; i { let start = Date.now(); // 模拟耗时操作(异步) setTimeout(() => { for(let i = 0; i { let start = Date.now(); // 模拟耗时操作(异步) setTimeout(() => { for (let i = 0; i { let start = Date.now(); // 模拟耗时操作(异步) setTimeout(() => { for (let i = 0; i { try{ this.controller.refresh(); } catch (error) { console.error(`ErrorCode:${(error as BusinessError).code},Message:${(error as BusinessError).message}`); } }) Button('Register JavaScript To Window') .onClick(()=>{ try { //只注册同步函数 this.controller.registerJavaScriptProxy(this.webTestObj,"objTestName",["webTest","webString"]); } catch (error) { console.error(`ErrorCode:${(error as BusinessError).code},Message:${(error as BusinessError).message}`); } }) Web({src: $rawfile('index.html'),controller: this.controller}).javaScriptAccess(true) } } }
- H5侧调用JSBridge函数
Document Click Me! async function htmlTest() { document.getElementById("demo").innerHTML = '测试开始:' + new Date().getTime() + '\n'; const time1 = new Date().getTime(); objTestName.webString(); const time2 = new Date().getTime(); objAsyncName.asyncString(); const time3 = new Date().getTime(); objName.asyncTestBool(); const time4 = new Date().getTime(); objName.test(); const time5 = new Date().getTime(); objTestName.webTest(); const time6 = new Date().getTime(); objAsyncName.asyncTest(); const time7 = new Date().getTime(); const result = [ 'objTestName.webString()耗时:'+ (time2 - time1), 'objAsyncName.asyncString()耗时:'+ (time3 - time2), 'objName.asyncTestBool()耗时:'+ (time4 - time3), 'objName.test()耗时:'+ (time5 - time4), 'objTestName.webTest()耗时:'+ (time6 - time5), 'objAsyncName.asyncTest()耗时:'+ (time7 - time6) ] document.getElementById("demo").innerHTML = document.getElementById("demo").innerHTML + '\n' + result.join('\n'); }
案例二:使用registerJavaScriptProxy或javaScriptProxy注册异步函数或异步同步共存,H5侧调用JSBridge函数与不推荐用法一致。
// registerJavaScriptProxy方式注册 Button('refresh') .onClick(()=>{ try{ this.controller.refresh(); } catch (error) { console.error(`ErrorCode:${(error as BusinessError).code},Message:${(error as BusinessError).message}`) } }) Button('Register JavaScript To Window') .onClick(()=>{ try { // 调用注册接口对象及成员函数,其中同步函数列表必填,空白则需要用[]占位;异步函数列表非必填 // 同步、异步函数都注册 this.controller.registerJavaScriptProxy(this.testObjtest,"objName",["test"],["asyncTestBool"]); // 只注册异步函数,同步函数列表处留空 this.controller.registerJavaScriptProxy(this.asyncTestObj,"objAsyncName",[],["asyncTest","asyncString"]); } catch (error) { console.error(`ErrorCode:${(error as BusinessError).code},Message:${(error as BusinessError).message}`); } }) Web({src: $rawfile('index.html'),controller: this.controller}).javaScriptAccess(true) // javaScriptProxy方式注册 // javaScriptProxy只支持注册一个对象,若需要注册多个对象请使用registerJavaScriptProxy Web({src: $rawfile('index.html'),controller: this.controller}) .javaScriptAccess(true) .javaScriptProxy({ object: this.testObjtest, name:"objName", methodList: ["test","toString"], //指定异步函数列表 asyncMethodList: ["test","toString"], controller: this.controller })
总结
数据运行结果如下:
注册方法类型 | 耗时(局限不同设备和场景,数据仅供参考) | 说明 |
---|---|---|
同步方法 | 1398ms,2707ms,2705ms | 同步函数调用会阻塞JavaScript线程 |
异步方法 | 2ms,2ms,4ms | 异步函数调用不阻塞JavaScript线程 |
通过运行数据可看到async的异步方法不需要等待结果,所以在JavaScript单线程任务队列中不会长时间占用,同步任务需要等待原生主线程同步执行后返回结果。
说明
JSBridge接口在注册时,即会根据注册调用的接口决定其调用方式(同步/异步)。开发者需根据当前业务区分, 是否将其注册为异步函数。
- 同步函数调用将会阻塞JavaScript的执行,等待调用的JSBridge函数执行结束,适用于需要返回值,或者有时序问题等场景。
- 异步函数调用时不会等待JSBridge函数执行结束,后续JavaScript可在短时间后继续执行。但JSBridge函数无法直接返回值。
- 注册在ETS侧的JSBridge函数调用时需要在主线程上执行;NDK侧注册的函数将在其他线程中执行。
- 异步JSBridge接口与同步接口在JavaScript侧的调用方式一致,仅注册方式不同,本部分调用方式仅作简要示范。
附NDK接口实现JSBridge通信(C++侧注册异步函数):
// 定义JSBridge函数 static void ProxyMethod1(const char* webTag, void* userData) { OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", "Method1 webTag :%{public}s",webTag); } static void ProxyMethod2(const char* webTag, void* userData) { OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", "Method2 webTag :%{public}s",webTag); } static void ProxyMethod3(const char* webTag, void* userData) { OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "ArkWeb", "Method3 webTag :%{public}s",webTag); } void RegisterCallback(const char *webTag) { int myUserData = 100; //创建函数方法结构体 ArkWeb_ProxyMethod m1 = { .methodName = "method1", .callback = ProxyMethod1, .userData = (void *)&myUserData }; ArkWeb_ProxyMethod m2 = { .methodName = "method2", .callback = ProxyMethod2, .userData = (void *)&myUserData }; ArkWeb_ProxyMethod m3 = { .methodName = "method3", .callback = ProxyMethod3, .userData = (void *)&myUserData }; ArkWeb_ProxyMethod methodList[2] = {m1,m2}; //创建JSBridge对象结构体 ArkWeb_ProxyObject obj = { .objName = "ndkProxy", .methodList = methodList, .size = 2 }; // 获取ArkWeb_Controller API结构体 ArkWeb_AnyNativeAPI* apis = OH_ArkWeb_GetNativeAPI(ArkWeb_NativeAPIVariantKind::ARKWEB_NATIVE_CONTROLLER); ArkWeb_ControllerAPI* ctrlApi = reinterpret_cast(apis); // 调用注册接口,注册函数 ctrlApi->registerJavaScriptProxy(webTag, &obj); ArkWeb_ProxyMethod asyncMethodList[1] = {m3}; ArkWeb_ProxyObject obj2 = { .objName = "ndkProxy", .methodList = asyncMethodList, .size = 1 }; ctrlApi->registerAsyncJavaScriptProxy(webTag, &obj2); }
同层渲染
同层渲染是一种优化技术,用于提高Web页面的渲染性能。同层渲染会将位于同一个图层的元素一起渲染,以减少重绘和重排的次数,从而提高页面的渲染效率。
总结
本文深入探讨了Web页面加载的原理和优化方法,为开发者提供了重要的指导和思路。在当今互联网时代,用户对网页加载速度和体验要求越来越高,因此页面加载优化成为开发者必须重视的一环。通过理解Web页面加载的原理,开发者可以更好地处理页面加载与优化的相关问题,提升应用的整体质量。
文中提供了预连接、预下载、预渲染、预取POST、预编译等多种常见的优化方法,指导开发者优化Web页面的加载速度。这些方法可以有效提高应用流畅度、提升用户体验。但是,这几种方法都是基于预处理的方式进行优化的,所以存在一定的优化代价。
在实际的开发场景中,开发者应该根据实际的情况进行权衡利弊,决定对应的方案与策略。此外,还提供了JSBridge与资源加速的优化方案,帮助开发者进一步提高Web加载性能。除了以上提到的优化方法,开发者还可以通过其他方式进一步优化页面加载速度。例如,压缩资源可以减小文件大小,减少加载时间;减少HTTP请求可以减少网络延迟,加快页面加载速度,提升用户体验。
综上所述,Web页面加载优化对于提升用户体验、提高网站性能、增加页面浏览量和提高转化率具有重要意义。开发者应该重视页面加载优化,不断探索和实践各种优化方法,以提升用户体验,实现商业目标。通过文章介绍的几种优化方法,开发者可以改善页面加载速度,提升用户体验,增加页面浏览量,提高应用的活跃度和用户粘性。只有不断优化页面加载速度,才能更好地满足用户需求,提升应用价值。