鸿蒙开发5.0案例分析:Web场景性能优化(二)
本文深入探讨了Web页面加载的原理和优化方法,为开发者提供了重要的指导和思路。在当今互联网时代,用户对网页加载速度和体验要求越来越高,因此页面加载优化成为开发者必须重视的一环。通过理解Web页面加载的原理,开发者可以更好地处理页面加载与优化的相关问题,提升应用的整体质量。文中提供了预连接、预下载、预渲染、预取POST、预编译等多种常见的优化方法,指导开发者优化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 })
}
}
}
前端页面代码:
<!DOCTYPE html>
<html>
<body>
<button type="button" onclick="callArkTS()">Click Me!</button>
<h1 id="text">这是一个测试信息,默认字体为黑色,调用runJavaScript方法后字体为绿色,调用runJavaScriptCodePassed方法后字体为红色</h1>
<script>
// 调用有参函数时实现。
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();
}
</script>
</body>
</html>
点击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<void *>(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作为应用前端页面:
<!DOCTYPE html>
<html lang="en-gb">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>run javascript demo</title>
</head>
<body>
<h1>run JavaScript Ext demo</h1>
<p id="webDemo"></p>
<br>
<button type="button" style="height:30px;width:200px" onclick="testNdkProxyObjMethod1()">test ndk method1 ! </button>
<br>
<br>
<button type="button" style="height:30px;width:200px" onclick="testNdkProxyObjMethod2()">test ndk method2 ! </button>
<br>
</body>
<script type="text/javascript">
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);
}
</script>
</html>
点击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 < 500000; i++) {}
let end = Date.now();
console.log('objName.test start: ' + start);
return 'objName.test Sync function took ' + (end - start) + 'ms';
}
asyncTestBool(testBol:boolean): Promise<string> {
return new Promise((resolve, reject) => {
let start = Date.now();
// 模拟耗时操作(异步)
setTimeout(() => {
for(let i = 0; i < 500000; i++) {}
let end = Date.now();
console.log('objAsyncName.asyncTestBool start: ' + start);
resolve('objName.asyncTestBool Async function took ' + (end - start) + 'ms');
}, 0); // 使用0毫秒延迟来模拟立即开始的异步操作
});
}
}
class WebObj {
constructor() {}
webTest(): string {
let start = Date.now();
// 模拟耗时操作
for(let i = 0; i < 500000; i++) {}
let end = Date.now();
console.log('objTestName.webTest start: ' + start);
return 'objTestName.webTest Sync function took ' + (end - start) + 'ms';
}
webString(): string {
let start = Date.now();
// 模拟耗时操作
for(let i = 0; i < 500000; i++) {}
let end = Date.now();
console.log('objTestName.webString start: ' + start);
return 'objTestName.webString Sync function took ' + (end - start) + 'ms';
}
}
class AsyncObj {
constructor() {
}
asyncTest(): Promise<string> {
return new Promise((resolve, reject) => {
let start = Date.now();
// 模拟耗时操作(异步)
setTimeout(() => {
for (let i = 0; i < 500000; i++) {
}
let end = Date.now();
console.log('objAsyncName.asyncTest start: ' + start);
resolve('objAsyncName.asyncTest Async function took ' + (end - start) + 'ms');
}, 0); // 使用0毫秒延迟来模拟立即开始的异步操作
});
}
asyncString(testStr:string): Promise<string> {
return new Promise((resolve, reject) => {
let start = Date.now();
// 模拟耗时操作(异步)
setTimeout(() => {
for (let i = 0; i < 500000; i++) {
}
let end = Date.now();
console.log('objAsyncName.asyncString start: ' + start);
resolve('objAsyncName.asyncString Async function took ' + (end - start) + 'ms');
}, 0); // 使用0毫秒延迟来模拟立即开始的异步操作
});
}
}
@Entry
@Component
struct Index {
controller: webview.WebviewController = new webview.WebviewController();
@State testObjtest: TestObj = new TestObj();
@State webTestObj: WebObj = new WebObj();
@State asyncTestObj: AsyncObj = new AsyncObj();
build() {
Column() {
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.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函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button type="button" onclick="htmlTest()"> Click Me!</button>
<p id="demo"></p>
<p id="webDemo"></p>
<p id="asyncDemo"></p>
</body>
<script type="text/javascript">
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');
}
</script>
</html>
案例二:使用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<ArkWeb_ControllerAPI*>(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页面加载优化对于提升用户体验、提高网站性能、增加页面浏览量和提高转化率具有重要意义。开发者应该重视页面加载优化,不断探索和实践各种优化方法,以提升用户体验,实现商业目标。通过文章介绍的几种优化方法,开发者可以改善页面加载速度,提升用户体验,增加页面浏览量,提高应用的活跃度和用户粘性。只有不断优化页面加载速度,才能更好地满足用户需求,提升应用价值。
更多推荐
所有评论(0)