〇. 概述
小程序一直以来采用的都是 AppService
和 WebView
的双线程模型,基于 WebView
和原生控件混合渲染的方式,小程序优化扩展了 Web 的基础能力,保证了在移动端上有良好的性能和用户体验。Web 技术至今已有 30 多年历史,作为一款强大的渲染引擎,它有着良好的兼容性和丰富的特性。 尽管各大厂商在不断优化 Web 性能,但由于其繁重的历史包袱和复杂的渲染流程,使得 Web 在移动端的表现与原生应用仍有一定差距。
为了进一步优化小程序性能,提供更为接近原生的用户体验,微信在 WebView
渲染之外新增了一个渲染引擎Skyline
, Skyline
在微信小程序基础库3.0.0发布时 (2023年7月5日) 正式启用。它采用了新的架构设计,优化了小程序扩展Web的基础能力,保证了在移动端上有良好的性能和用户体验。Skyline
,其使用更精简高效的渲染管线,并带来诸多增强特性,让 Skyline
拥有更接近原生渲染的性能体验。
官方Skyline
体验demo:
一. 新架构
Webview 下的双线程模型
小程序运行环境分成渲染层 和逻辑层 ,其中WXML
模板和WXSS
样式工作在渲染层,JS
脚本工作在逻辑层。
小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView
进行渲染;逻辑层采用JsCore
线程运行JS
脚本。一个小程序存在多个界面,所以渲染层存在多个WebView
线程,这两个线程的通信会经由微信客户端(下文中也会采用Native
来代指微信客户端)做中转,逻辑层发送网络请求也经由Native
转发,小程序的通信模型下图所示。
问题:
WebView
因为历史原因存在渲染管线臃肿的问题,导致在移动端的表现与原生应用仍然有差距。- 每个页面都需要实例化一个
JS
引擎(WebView
),导致内存占用多,影响应用性能,因此小程序对打开的页面数量有限制(页面栈最多10个)。 - 渲染层和逻辑层使用
JSBridge
通信,在复杂场景下(比如拖动元素的交互动画)会存在性能问题,不过可以通过WXS
来解决这个问题,但直接去除JSBridge
会更好。
Skyline
新架构
当小程序基于 WebView
环境下时,WebView
的 JS 逻辑、DOM 树创建、CSS 解析、样式计算、Layout、Paint (Composite) 都发生在同一线程,在 WebView
上执行过多的 JS 逻辑可能阻塞渲染,导致界面卡顿。以此为前提,小程序同时考虑了性能与安全,采用了目前称为「双线程模型」的架构。
在 Skyline
环境下,我们尝试改变这一情况:Skyline
创建了一条渲染线程来负责 Layout, Composite 和 Paint 等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView
承担的 JS 逻辑、DOM 树创建等逻辑。这种新的架构相比原有的 WebView
架构,有以下特点:
- 界面更不容易被逻辑阻塞,进一步减少卡顿
- 无需为每个页面新建一个 JS 引擎实例(WebView),减少了内存、时间开销
- 框架可以在页面之间共享更多的资源,进一步减少运行时内存、时间开销
- 框架的代码之间无需再通过 JSBridge 进行数据交换,减少了大量通信时间开销
而与此同时,这个新的架构能很好地保持和原有架构的兼容性,基于 WebView 环境的小程序代码基本上无需任何改动即可直接在新的架构下运行。WXS 由于被移到 AppService 中,虽然逻辑本身无需改动,但询问页面信息等接口会变为异步,效率也可能有所下降;为此,微信同时推出了新的 Worklet 机制,它比原有的 WXS 更靠近渲染流程,用以高性能地构建各种复杂的动画效果。
新的渲染流程如下图所示:
ps: skyline 使用的可能是 flutter 绘制方案, 见: www.zhihu.com/question/54...
Skyline 支持与 WebView 混合使用
小程序支持页面使用 WebView 或 Skyline 任一模式进行渲染,Skyline 页面可以和 WebView 页面混跳,开发者可以页面粒度按需适配 Skyline。
js
// page.json
// skyline 渲染
{
"renderer": "skyline"
}
// webview 渲染
{
"renderer": "webview"
}
二. 新特性
worklet
动画函数
在双线程模式里,为了解决交互动画(如拖动元素)时线程间通信的性能问题,引入了 WXS,让部分 JS 逻辑放到 WebView 里执行,以此解决通信时的性能问题,但在 skyline 里,WXS 被移动到了 AppService 中,导致效率会有所下降,所以推出 Worklet 进行替代
使用 worklet
动画能力时确保以下两项:
- 确保开发者工具右上角 > 详情 > 本地设置里的
将 JS 编译成 ES5
选项被勾选上 (代码包体积会少量增加) - worklet 动画相关接口仅在
Skyline
渲染模式下才能使用
概念一:worklet
函数
worklet 函数是一种声明在开发者代码中,可运行在 JS
线程或 UI
线程的函数,函数体顶部有 'worklet'
指令声明, 而非 worklet 函数只能运行在 JS 线程中。
js
function someWorklet(greeting) {
'worklet';
console.log(greeting);
}
// 运行在 JS 线程
someWorklet('hello') // print: hello
// 运行在 UI 线程
wx.worklet.runOnUI(someWorklet)('hello') // print: [ui] hello
worklet 函数间相互调用
js
const name = 'skyline'
function anotherWorklet() {
'worklet';
return 'hello ' + name;
}
// worklet 函数间可互相调用
function someWorklet() {
'worklet';
const greeting = anotherWorklet();
console.log('another worklet says ', greeting);
}
wx.worklet.runOnUI(someWorklet)() // print: [ui] another worklet says hello skyline
从 UI 线程调回到 JS 线程
js
function someFunc(greeting) {
console.log('hello', greeting);
}
function someWorklet() {
'worklet';
// 访问非 worklet 函数时,需使用 runOnJS
// someFunc 运行在 JS 线程
runOnJS(someFunc)('skyline');
}
wx.worklet.runOnUI(someWorklet)(); // print: hello skyline
概念二:共享变量
共享变量是指在 JS
线程创建,可在两个线程间同步的变量。
js
const { shared, runOnUI } = wx.worklet
const offset = shared(0)
function someWorklet() {
'worklet'
console.log(offset.value) // print: 1
// 在 UI 线程修改
offset.value = 2
console.log(offset.value) // print: 2
}
// 在 JS 线程修改
offset.value = 1
runOnUI(someWorklet)()
由 shared
函数创建的变量,我们称为 sharedValue
共享变量。用法上可类比 vue3
中的 ref
,对它的读写都需要通过 .value
属性,但需注意的是它们并不是一个概念。sharedValue
的用途主要如下。
跨线程共享数据
由 worklet
函数捕获的外部变量,实际上会被序列化后生成在 UI
线程的拷贝,如下代码中, someWorklet
捕获了 obj
变量,尽管我们修改了 obj
的 name
属性,但在 someWorklet
声明的位置,obj
已经被序列化发送到了 UI
线程,因此后续的修改是无法同步的。
js
const obj = { name: 'skyline'}
function someWorklet() {
'worklet'
console.log(obj.name) // 输出的仍旧是 skyline
}
obj.name = 'change name'
wx.worklet.runOnUI(someWorklet)()
sharedValue
就是用来在线程间同步状态变化的变量。
js
const { shared, runOnUI } = wx.worklet
const offset = shared(0)
function someWorklet() {
'worklet'
console.log(offset.value) // 输出的是新值 1
}
offset.value = 1
runOnUI(someWorklet)()
驱动动画
worklet
函数和共享变量就是用来解决交互动画问题的。相关接口 applyAnimatedStyle
可通过页面/组件实例访问,接口文档参考。
js
<view id="moved-box"></view>
<view id="btn" bind:tap="tap">点击驱动小球移动</view>
Page({
onLoad() {
const offset = wx.worklet.shared(0)
this.applyAnimatedStyle('#moved-box', () => {
'worklet';
return {
transform: `translateX(${offset.value}px)`
}
})
this._offset = offset
},
tap() {
// 点击时修改 sharedValue 值,驱动小球移动
this._offset.value = Math.random()
}
})
当点击按钮 #btn
时,我们用随机数给 offset
进行赋值,小球会随之移动。
applyAnimatedStyle
接口的第二个参数 updater
为一个 worklet
函数,其捕获了共享变量 offset
,当 offset
的值变化时,updater
会重新执行,并将返回的新 styleObject
应用到选中节点上。
当然,光看这个例子,跟用 setData
看好像没有什么区别。但当 worklet
动画和手势结合时,就产生了质变。
手势处理
js
<pan-gesture-handler onGestureEvent="handlepan">
<view class="circle"></view>
</pan-gesture-handler>
Page({
onLoad() {
const offset = wx.worklet.shared(0);
this.applyAnimatedStyle('.circle', () => {
'worklet';
return {
transform: `translateX${offset.value}px`
};
});
this._offset = offset;
},
handlepan(evt) {
'worklet';
if (evt.state === GestureState.ACTIVE) {
this._offset.value += evt.deltaX;
}
}
});
当手指在 circle
节点上移动时,会产生平滑的拖动效果。handlepan
回调触发在 UI
线程,同时我们修改了 offset
的值,会在 UI
线程产生动画,不必再绕回到 JS
线程。
手势系统
业务开发中,我们常需要监听节点 touch
事件,处理拖拽、缩放相关逻辑。由于 Skyline
采用双线程架构,在进行这样的交互动画时,会具有较大的异步延迟,这点可以参考 wxs 响应事件。
Skyline
中 wxs
代码运行在 JS
线程,而事件产生在 UI
线程,因此 wxs 动画
性能有所降低,为了提升小程序交互体验的效果,我们内置了一批手势组件,使用手势组件的优势包括
- 免去开发者监听
touch
事件,自行计算手势逻辑的复杂步骤 - 手势组件直接在
UI
线程响应,避免了传递到JS
线程带来的延迟
效果演示:
自定义路由
小程序采用多 WebView
架构,页面间跳转形式十分单一,仅能从右到左进行动画。而原生 App
的动画形式则多种多样,如从底部弹起,页面下沉,半屏等。
Skyline
渲染引擎下,页面有两种渲染模式: WebView
和 Skyline
,它们通过页面配置中的 renderer
字段进行区分。在连续的 Skyline
页面间跳转时,可实现自定义路由效果。
为降低开发成本,基础库预设了一批常见的路由动画效果, 见: Skyline 渲染引擎 / 增强特性 / 预设路由效果 (qq.com)
仅需在路由跳转时,指定对应的 routeType
js
wx.navigateTo({
url: 'xxx',
routeType: 'wx://modal'
})
共享元素动画
原生 App
中我们常见到这样的交互,如从商品列表页进入详情页过程中,商品图片在页面间飞跃,使得过渡效果更加平滑,另一个案例是朋友圈的图片预览放大功能。在 Skyline
渲染模式下,我们称其为共享元素动画,可通过 share-element
组件来实现。
在连续的 Skyline
页跳转时,页面间 key
相同的 share-element
节点将产生飞跃特效,开发者可自定义插值方式和动画曲线。通常作用于图片,为保证动画效果,前后页面的 share-element
子节点结构应该尽量保持一致。
内置组件更新
- 提供 grid-view 瀑布流组件。
瀑布流是一种常用的列表布局方式,得益于 Skyline 在布局过程中的可控性,我们直接在底层实现并提供出来,渲染性能要比 WebView 更优。
- 提供 snapshot 截图组件。
大多数小程序都会基于 canvas 实现自定义分享图的功能,一方面,需要通过 canvas 绘图指令手动实现,较为繁琐;另一方面,在分享图的布局较复杂时,或者在制作长图时会受限于系统对 canvas 尺寸限制,canvas 的方案实现成本都会很大。得益于 Skyline 在渲染过程中的可控性,Skyline 能直接对 WXML 子树进行截图,因此我们直接提供了截图组件,这样能复用更完善的 WXSS 能力,极大降低开发成本。
- scroll-view 组件支持列表反转。
在聊天对话的场景下,列表的滚动常常是反向的(往底部往上滚动),若使用正向滚动来模拟会有很多多余的逻辑,而且容易出现跳动,而 scroll-view 提供的 reverse 属性很好的解决这一问题。
三. 兼容性及最佳实践
几个比较大的兼容性问题
- 不支持原生导航栏, 需自行实现自定义导航栏
- 不支持页面全局滚动, 在需要滚动的区域使用 scroll-view 实现
- 暂不支持fixed定位, em单位,
- 不支持通配选择器, 属性选择器, 兄弟选择器等
- 默认值与webview差异较大, 如默认
flex
布局且flex-flow: column;
, 默认border-box
盒模型等
四. 总结
优点
- 接近原生的交互体验 (手势系统, 自定义路由, 共享元素)
- 节省内存, 性能提升 (wxss预编译, 底层为flutter)
- 组件更新带来的新功能 (如截图组件)
缺点
- 新技术稳定性差, 目前不太稳定
- css很多属性在styline的wxss下不支持, 开发心智负担大
- 新的worklet函数, 自定义路由等语法可读性差, 学习成本高