微信小程序新增渲染引擎 Skyline

3,941 阅读6分钟

背景

为了进一步提升微信小程序的性能,解决当前双线程模型的一些问题,微信小程序基础库 3.0.0 版本带来了新的渲染引擎 Skyline,以下我们通过几个问答来解释 Skyline 是什么以及帮我们解决了什么问题。

什么是双线程模型?

就是逻辑层和渲染层分别在独立的线程,在渲染层运行 WXML 模板和 WXSS 样式,使用 WebView 进行渲染,在逻辑层运行 JS 脚本,使用 JSCore 进行执行。可以参考下图:

image.png

双线程模型有什么问题?

  1. WebView 因为历史原因存在渲染管线臃肿的问题,导致在移动端的表现与原生应用仍然有差距。
  2. 每个页面都需要实例化一个 JS 引擎(WebView),导致内存占用多,影响应用性能,因此小程序对打开的页面数量有限制。
  3. 渲染层和逻辑层使用 JSBridge 通信,在复杂场景下(比如拖动元素的交互动画)会存在性能问题,不过可以通过 WXS 来解决这个问题,但直接去除 JSBridge 会更好。

什么是渲染引擎 Skyline?

在双线程模式里,渲染层(WebView)负责DOM 树创建、CSS 解析、样式计算、Layout、composite 和 Paint 等渲染任务,逻辑层(AppService 线程)负责执行 JS 逻辑,而在 Skyline 里,新增渲染线程负责Layout、composite 和 Paint 等渲染任务,而 AppService 线程负责执行 JS 逻辑,DOM 树创建等任务。可以参考下图:

image.png

Skyline 是如何解决这些问题的?

在 Skyline 里只需要实例化一个 JS 引擎,减少内存占用,并且线程之间可以直接通信,而不需要借助 JSBridge,减少大量通信时间开销。并且精简了渲染管线,进一步提升渲染性能。

如何使用

可以参考我之前写的一篇文章如何将微信小程序从WebView迁移到Skyline

增强特性

Skyline 基本可以兼容之前的代码(部分需要手动兼容),并且还带来了一些特性,比如 worklet 动画、手势系统、自定义路由、共享元素等,其中最让人眼前一亮的自定义路由,丰富了页面切换的效果,带来了之前只有在原生应用上才有的动画。下面会按照顺序分别介绍下,并提供官方 Demo。

Worklet 动画

在双线程模式里,为了解决交互动画(如拖动元素)时线程间通信的性能问题,引入了 WXS,让部分 JS 逻辑放到 WebView 里执行,以此解决通信时的性能问题,但在 skyline 里,WXS 被移动到了 AppService 中,导致效率会有所下降,所以推出 Worklet 进行替代。

Worklet 动画是为了解决交互动画问题,为此引入了2个新的概念

概念一: worklet 函数

worklet 函数可运行在 JS 线程或者 UI 线程的函数,在函数顶部使用 worklet 指令声明即可,而非 worklet 函数只能运行在 JS 线程中。

// 非 worklet 函数
function someFunc(greeting) {
  console.log('hello', greeting);
}

// worklet 函数
function otherFunc() {
  'worklet';
  console.log('hello otherFunc');
}

function someWorklet() {
  'worklet'
  
  // 在 worklet 函数里访问 worklet 函数时,可以直接调用,otherFunc 运行在 UI 线程
  otherFunc()
  
  // 访问非 worklet 函数时,需使用 runOnJS,将 someFunc 运行在 JS 线程
  runOnJS(someFunc)('skyline') 
}

someFunc('skyline') // print: hello skyline,将 someFunc 运行在 JS 线程

otherFunc() // print: hello world,将 otherFunc 运行在 JS 线程

wx.worklet.runOnUI(someWorklet)() // print: hello skyline,将 someWorklet 运行在 UI 线程

概念二: 共享变量

因为由 worklet 函数捕获的外部变量,实际上会被序列化后生成在 UI 线程的拷贝,导致在 JS 线程里的后续修改是无法同步的,例如:

const obj = { name: 'skyline'}
function someWorklet() {
  'worklet'
  console.log(obj.name) // 输出的仍旧是 skyline
}
obj.name = 'change name'

wx.worklet.runOnUI(someWorklet)() 

这时候就需要使用共享变量,例如:

onst { shared, runOnUI } = wx.worklet

const offset = shared(0)
function someWorklet() {
  'worklet'
  console.log(offset.value) // 输出的是新值 1
}
offset.value = 1

runOnUI(someWorklet)() 

而共享变量最大的使用场景就是在交互动画中,例如:

<pan-gesture-handler onGestureEvent="handlepan">
  <view class="circle"></view>
</pan-gesture-handler>
Page({
  onLoad() {
    // 创建共享变量 offset
    const offset = wx.worklet.shared(0);
    // 绑定由 worklet 驱动的样式到相应的节点,并且绑定 worklet 函数,当 offset 变化时,该函数会被调用。并且应该该函数 和 handlepan 都是在 UI 线程执行,则不需要绕回 JS 线程。
    this.applyAnimatedStyle('.circle', () => {
      'worklet';
      return {
        transform: `translateX${offset.value}px`
      };
    });
    this._offset = offset;
  },
  handlepan(evt) {
    'worklet';
    if (evt.state === GestureState.ACTIVE) {
      // 接收到事件的变化,修改 offset 值
      this._offset.value += evt.deltaX;
    }
  }
});

手势系统

带来做大的特性是允许代理原生组件内部手势(比如 scroll-view 和 swiper),结合手势协商,就可以实现更为复杂的交互。

例如视频号的评论列表:

image.png

<view class="page-container">
	<view class="placehodler-widget" />
	<pan-gesture-handler
	 tag="pan"
	 shouldResponseOnMove="shouldPanResponse"
	 simultaneousHandlers="{{['scroll']}}"
	 onGestureEvent="handlePan"
	>
		<vertical-drag-gesture-handler
		 tag="scroll"
		 native-view="scroll-view"
		 shouldResponseOnMove="shouldScrollViewResponse"
		 simultaneousHandlers="{{['pan']}}"
		>
			<scroll-view
			 class="list-wrp"
			 scroll-y
			 bounces="{{false}}"
			 refresher-enabled="{{false}}"
			 adjustDecelerationVelocity="adjustDecelerationVelocity"
			 bindscroll="handleScroll"
			 type="list"
			>
				<view class="item" wx:for="{{list}}">
					<view class="avatar" />
					<view class="comment" />
				</view>
			</scroll-view>
		</vertical-drag-gesture-handler>
	</pan-gesture-handler>
</view>
Page({
    /**
     * 生命周期函数--监听页面加载
     */
    onLoad(options) {
        const transY = shared(0);
        // 拖动列表
        this.applyAnimatedStyle('.list-wrp', () => {
            'worklet';
            return {
                transform: `translateY(${transY.value}px)`
            };
        });
        this.transY = transY;
        this.scrollTop = shared(0); // 记录列表滚动距离
        this.startPan = shared(false); // 是否拖动列表
    },

    // 是否需要拖动列表,如果返回 true,则响应拖动列表
    shouldPanResponse() {
        'worklet';
        return this.startPan.value;
    },

    // 是否需要滚动列表,如果返回 true,则响应滚动列表
    shouldScrollViewResponse(pointerEvent) {
        'worklet';
        if (this.transY.value > 0) return false;// 如果在拖动列表,则禁止响应滚动列表
        const scrollTop = this.scrollTop.value;
        const {
            deltaY
        } = pointerEvent;
        const result = !(scrollTop <= 0 && deltaY > 0);// 在滚动中或者向上滑动,则响应滚动列表
        this.startPan.value = !result;
        return result;
    },

    // 监听到列表拖动事件
    handlePan(evt) {
        'worklet';
        if (evt.state === GestureState.ACTIVE) {
            const curPosition = this.transY.value;
            const destination = Math.max(0, curPosition + evt.deltaY);
            if (curPosition === destination) return;
            this.transY.value = destination;
        }
        
        // 如果手势结束,则恢复位置
        if (evt.state === GestureState.END || evt.state === GestureState.CANCELLED) {
            this.transY.value = timing(0);
            this.startPan.value = false;
        }
    },

    adjustDecelerationVelocity(velocity) {
        'worklet';
        const scrollTop = this.scrollTop.value;
        return scrollTop <= 0 ? 0 : velocity;
    },

    // 监听到列表滚动事件
    handleScroll(evt) {
        'worklet';
        this.scrollTop.value = evt.detail.scrollTop;
    },
})

具体可以参考 demo-negotiation

自定义路由

支持页面跳转动画

伪代码如下:

// 配置 Route Builder
const HalfScreenDialogRouteBuilder = (customRouteContext) => {
  /** 
   * 操作是从 A 页面进入 B 页面
   * 进入动画 t: 1->0,退出动画 t: 0->1,B 页面
   * 1. 手势拖动时采用原始值
   * 2. 页面进入时采用 curve 曲线生成的值
   * 3. 页面返回时采用 reverseCurve 生成的值
   */
  const handlePrimaryAnimation = () => {
    'worklet'
    let t = primaryAnimation.value
    if (!userGestureInProgress.value) {
      t = _curvePrimaryAnimation.value
    }

    // 距离顶部边距因子
    const topDistance = 0.12
    // 距离顶部边距
    const marginTop = topDistance * screenHeight
    // 半屏页面大小
    const pageHeight = (1 - topDistance) * screenHeight
    // 自底向上显示页面
    const transY = pageHeight * (1 - t)

    const style = {
      overflow: 'hidden',
      borderRadius: '10px',
      marginTop: `${marginTop}px`,
      height: `${pageHeight}px`,
      transform: `translateY(${transY}px)`,
    }

    if (!isSupportOverflow) delete style.overflow
    return style 
  }

  /**
   * 操作是从 A 页面进入 B 页面
   * 压入动画 t: 1->0,压出动画 t: 0->1 A 页面
   */
  const handlePreviousPageAnimation = () => {
    'worklet'
    let t = primaryAnimation.value
    if (!userGestureInProgress.value) {
      t = _curvePrevAnimation.value
    }

    // 页面缩放大小
    const scale = 0.08
    // 距离顶部边距因子
    const topDistance = 0.1
    // 估算的偏移量
    const transY = screenHeight * (topDistance - 0.5 * scale) * t
    const radius = 12 * t
    const scaleValue = 1 - scale * t

    // skyline 1.0.1 版本以下修改 overflow: hidden 有问题
    const style = {
      borderRadius: `${radius}px`,
      overflow: radius > 0 ? 'hidden' : 'visible',
      transform: `translateY(${transY}px) scale(${scaleValue})`,
    }
    if (!isSupportOverflow) delete style.overflow
    return style 
  }

  return {
    opaque: false,
    transitionDuration: 300,
    reverseTransitionDuration: 300,
    canTransitionTo: true, // 是否与下一个页面联动
    canTransitionFrom: true, // 是否与前一个页面联动
    handlePrimaryAnimation, // 当前页面的动画
    handlePreviousPageAnimation, // 上一个页面的动画
  }
}

// 注册 Route Builder
wx.router.addRouteBuilder('HalfScreenDialog', HalfScreenDialogRouteBuilder)

// 跳转页面,从 A 页面进入 B 页面
wx.navigateTo({
    url: `/half-page/index?routeType=${routeType}`,
    routeType: 'HalfScreenDialog',
});

点击可查看更多 Skyline 示例

共享元素动画

共享元素 share-element,可以实现如从商品列表页进入详情页过程中,商品图片在页面间飞跃动画和朋友圈的图片预览放大功能。 image.png 仿朋友圈图片预览放大功能伪代码如下:

A 页面

<scroll-view type="custom" scroll-y class="scroll-view">
	<grid-view
	 type="aligned"
	 cross-axis-count="{{crossAxisCount}}"
	 main-axis-gap="{{gap}}"
	 cross-axis-gap="{{gap}}"
	>
		<block wx:for="{{thumbnailList}}" wx:key="id">
			<animated-image
       style="height: {{cellHeight}}px;"
			 key="{{item.key}}"
			 src="{{item.url}}"
			 data-key="{{item.key}}"
			 data-url="{{item.url}}"
			 bind:tap="goDetail"
			/>
		</block>
	</grid-view>
</scroll-view>

B 页面

<share-element
 class="img-wrp"
 key="{{key}}"
 shuttle-on-push="from"
 transition-on-gesture
 bind:tap="back"
>
	<image class="img" src="{{imgSrc}}" mode="aspectFit" />
</share-element>

参考 Demo

限制

目前 Skyline 还在完善当中,目前还存在一些支持上的限制和差异,具体看如下文档:

总结

渲染引擎 Skyline 在提升应用性能的同时,带来了类似于原生应用的丰富动画效果,非常值得大家尝试。

参考