微信小程序新渲染引擎Skyline介绍

官方文档: Skyline 渲染引擎 / 概览 / 介绍 (qq.com)

〇. 概述

小程序一直以来采用的都是 AppService 和 WebView 的双线程模型,基于 WebView 和原生控件混合渲染的方式,小程序优化扩展了 Web 的基础能力,保证了在移动端上有良好的性能和用户体验。Web 技术至今已有 30 多年历史,作为一款强大的渲染引擎,它有着良好的兼容性和丰富的特性。 尽管各大厂商在不断优化 Web 性能,但由于其繁重的历史包袱和复杂的渲染流程,使得 Web 在移动端的表现与原生应用仍有一定差距。

为了进一步优化小程序性能,提供更为接近原生的用户体验,微信在 WebView 渲染之外新增了一个渲染引擎Skyline, Skyline 在微信小程序基础库3.0.0发布时 (2023年7月5日) 正式启用。它采用了新的架构设计,优化了小程序扩展Web的基础能力,保证了在移动端上有良好的性能和用户体验。Skyline,其使用更精简高效的渲染管线,并带来诸多增强特性,让 Skyline 拥有更接近原生渲染的性能体验。

官方Skyline体验demo:

一. 新架构

Webview 下的双线程模型

官方介绍: 小程序宿主环境 | 微信开放文档 (qq.com)

小程序运行环境分成渲染层 和逻辑层 ,其中WXML模板和WXSS样式工作在渲染层,JS脚本工作在逻辑层。

小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型下图所示。

问题:

  1. WebView因为历史原因存在渲染管线臃肿的问题,导致在移动端的表现与原生应用仍然有差距。
  2. 每个页面都需要实例化一个JS引擎(WebView),导致内存占用多,影响应用性能,因此小程序对打开的页面数量有限制(页面栈最多10个)。
  3. 渲染层和逻辑层使用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 动画函数

文档: Skyline 渲染引擎 / 增强特性 / Worklet 动画 (qq.com)

在双线程模式里,为了解决交互动画(如拖动元素)时线程间通信的性能问题,引入了 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 动画 性能有所降低,为了提升小程序交互体验的效果,我们内置了一批手势组件,使用手势组件的优势包括

  1. 免去开发者监听 touch 事件,自行计算手势逻辑的复杂步骤
  2. 手势组件直接在 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 子节点结构应该尽量保持一致。

内置组件更新

  1. 提供 grid-view 瀑布流组件。

瀑布流是一种常用的列表布局方式,得益于 Skyline 在布局过程中的可控性,我们直接在底层实现并提供出来,渲染性能要比 WebView 更优。

  1. 提供 snapshot 截图组件。

大多数小程序都会基于 canvas 实现自定义分享图的功能,一方面,需要通过 canvas 绘图指令手动实现,较为繁琐;另一方面,在分享图的布局较复杂时,或者在制作长图时会受限于系统对 canvas 尺寸限制,canvas 的方案实现成本都会很大。得益于 Skyline 在渲染过程中的可控性,Skyline 能直接对 WXML 子树进行截图,因此我们直接提供了截图组件,这样能复用更完善的 WXSS 能力,极大降低开发成本。

  1. scroll-view 组件支持列表反转。

在聊天对话的场景下,列表的滚动常常是反向的(往底部往上滚动),若使用正向滚动来模拟会有很多多余的逻辑,而且容易出现跳动,而 scroll-view 提供的 reverse 属性很好的解决这一问题。

三. 兼容性及最佳实践

几个比较大的兼容性问题

  • 不支持原生导航栏, 需自行实现自定义导航栏
  • 不支持页面全局滚动, 在需要滚动的区域使用 scroll-view 实现
  • 暂不支持fixed定位, em单位,
  • 不支持通配选择器, 属性选择器, 兄弟选择器等
  • 默认值与webview差异较大, 如默认flex布局且flex-flow: column;, 默认border-box盒模型等

兼容建议: Skyline 渲染引擎 / 从 WebView 迁移 / 常见兼容问题 (qq.com)

最佳实践建议: Skyline 渲染引擎 / 从 WebView 迁移 / 最佳实践 (qq.com)

四. 总结

优点

  • 接近原生的交互体验 (手势系统, 自定义路由, 共享元素)
  • 节省内存, 性能提升 (wxss预编译, 底层为flutter)
  • 组件更新带来的新功能 (如截图组件)

缺点

  • 新技术稳定性差, 目前不太稳定
  • css很多属性在styline的wxss下不支持, 开发心智负担大
  • 新的worklet函数, 自定义路由等语法可读性差, 学习成本高
相关推荐
ngaiwe3 小时前
AA我来算小程序实践:把付款人、承担人和结算状态分开建模
微信小程序
上线再说13 小时前
小程序图片处理从 2.9 秒到 420 毫秒:云函数冷启动的 5 个坑
微信小程序
沃达德软件13 小时前
智慧教育教学资源管理系统(PC端+微信小程序)
微信小程序·小程序
考拉讲支付1 天前
延迟分账 / 核销分账的技术实现方案:服务完成后再分钱,到底怎么设计?
大数据·运维·人工智能·微信小程序·小程序·系统架构
科威网络2 天前
小程序开发性能优化:分包、预加载与启动时间
性能优化·微信小程序·小程序
互联网+人工智能观察3 天前
微信小程序私有化定制、源码交付方案选型|SaaS / 模板二开 / 定制方案技术对比
微信小程序·小程序
爱勇宝4 天前
小红花成长更新了:孩子想要什么,也应该有机会自己说出来
微信小程序·产品·小程序·云开发
andrsted5 天前
企业培训时间难统一?练题簿让在线免费刷题与培训考核更好安排
学习·微信小程序·小程序·学习方法·刷题
拖孩5 天前
一个人 + AI 做的小程序,今天,正式回本了
前端·后端·微信小程序
李纲明5 天前
微信小程序 wx.uploadFile 上传失败:合法域名、临时路径与 HTTPS 证书排查
微信小程序·小程序·uploadfile·devdiy·上传失败