想要优化小程序,必须要懂的小程序运行机制原理

2,030 阅读8分钟

运行环境与网页的区别

网页开发渲染线程和JS线程是互斥的,渲染线程和JS线程都可以操作DOM,容易引起混乱,当执行 JS 引擎线程时,GUI渲染线程会被挂起,当前任务队列为空时,JS引擎才会去执行 GUI 渲染。这也是为什么长时间的脚本运行可能会导致页面失去响应的原因。

此处不太理解的同学可以看我的另一篇文章,关于浏览器的线程与进程

而对于小程序,它的逻辑层和渲染层是分开的,小程序采用 AppServiceWebView 的双线程模型,基于 WebView 和原生控件混合渲染的方式,小程序的两个线程没有互斥的关系,js脚本线程和GUI渲染线程在 Native 客户端的协调下可以有条不紊的同时执行。

双线程模型运行机制

小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层则使用JavaScript引擎解析和执行JS逻辑代码。一个小程序存在多个页面,所以渲染层存在多个 WebView 线程。

渲染层多线程的原因,便于维护多页面的状态:
例如:一款新闻信息流小程序,它有两个页面,A页面是新闻信息流,B页面是新闻详情页。那么当用户往下滑了很久后发现一个感兴趣的新闻,点击跳转到详情页,看完之后想回到A页面刚才的位置继续浏览。单页应用由于把A页面给擦掉了,所以这种场景下当从B回到A时,会发现A又重新刷新了一遍,体验非常糟糕。

TIPS: 官方说渲染层和逻辑层分别是两个线程,个人觉得容易引起歧义,更严谨一点说是两个进程中的两个线程,即WebView进程和App进程,官方文档中有这样一句话:

当小程序基于 WebView 环境下时,WebView 的 JS 逻辑、DOM 树创建、CSS 解析、样式计算、Layout、Paint (Composite) 都发生在同一线程,在 WebView 上执行过多的 JS 逻辑可能阻塞渲染,导致界面卡顿。以此为前提,小程序同时考虑了性能与安全,采用了目前称为「双线程模型」的架构。

上面说的 “同一线程” 其实指的是webView的渲染进程(渲染进程中包含渲染线程和JS主线程等),网页开发中提到的渲染线程与JS主线程互斥和阻塞的问题在 WebView 环境下也不例外,所以小程序则将 JS 逻辑部分拆到App级(App进程中不同于WebView环境,并没有window document等对象)的JS线程中运行,即所谓的AppServiceWebViewApp是独立的运行环境,虽然不会发生阻塞,但是也无法共享数据,所以要依靠 Native 来通讯。

所以理论上是可以在wxs中访问到window对象的,因为wxs代码运行在 webview 中,但是微信对wxs功能做了阉割限制,尤其不能让用户操作DOM。

image.png

通讯过程如下:

两个线程的通信是基于微信客户端提供的WeixinJsBridge来实现的,像一个桥梁一样将小程序的运行环境和微信客户端(native连接了起来),同时也负责在渲染层和逻辑层之间传递数据和事件的工作。

数据传输通过逻辑层和视图层两边提供的evaluateJavascript实现的,用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边的独立环境。

请求也是通过WeixinJsBridge由微信客户端代为发起。

以上这样的运行机制,优缺点都很明显:

双线程模型的优缺点

优点:

  • 将逻辑层和渲染层隔离开,用户无法直接操作DOM,提供了相对封闭和安全的运行环境。
  • JS不会阻塞渲染,但是大部分情况下视觉都要依赖JS中处理的数据,JS阻塞时也不会通知视图去更新,所以这条优点其实意义不是很大
  • 所有的页面和组件的逻辑都在一个线程(AppService)里,使用同一个上下文环境,比较好做状态共享或跨页面通讯。

缺点:

  • 缺点在于每一次数据传递都要进行一次线程之间的通信,业务逻辑跟渲染层天然隔离,造成通信开销大、延迟高等问题,通信越频繁、数据量越大,则性能瓶颈越严重。

  • 每个页面都创建一个WebView线程处理,有更多的内存、时间开销。

  • 渲染层和逻辑层状态要维护两份,进一步加重内存、时间开销,并且没有办法完全保证两份数据状态实时保持一致,例如仅使用 this.data 更新数据而不是通过setData时,那么实际渲染的值与逻辑层的值就不一致,某些场景下会造成非预期的问题。

    尤其当 this.data.obj 这个obj指向一个引用地址的时候,带来的问题更加不可预期。

    this.viewModal = {obj: {a: 3}};
    this.setData({obj: this.viewModal.obj}); // 此时逻辑层的data.obj已经指向viewModal.obj对象的地址
    
    this.viewModal.obj.a = 666;
    this.data.obj.a === 666; // true
    // 之后无论你在任何地方不小心更改了this.viewModal.obj,那么this.data.obj也变了,而此时在渲染层的数据还是旧数据,你本意是想通过this.data获取当前被渲染的值,但是这时候可能值已经并不符合你的预期了。
    

小程序并非所有组件都是通过 webview 来渲染的。比如像 video, canvas, map 等称为 native compoennt 的组件是直接用客户端的原生组件渲染的,这意味着这些组件的性能更好。

总体上从开发者的角度来看,我认为这个架构并不是一个很好的架构,逻辑层与渲染层隔离,带来的问题远远比它解决的问题更多。

一个应用里面的多个页面,你认为是共享的状态多,还是独立的状态多?用户在操作一个页面时,更关心跨页面通信实时性,还是更关心当前页面的渲染实时性?

我们当然更关心性能,但是从微信的角度来说,他们想既能享受 web 生态的好处的同时也能限制 web 的开放性,增强自己对平台内容的管控程度,从禁用evalnew Function()上就能看出一二。

当然微信也知道架构所带来的性能问题,所以发明了 WXS ,让一部分 js 代码能在渲染层跑,部分解决通信消耗和延迟的问题,还提供了一系列的性能优化指南。

而近期,又有重磅更新放出,新渲染引擎 Skyline 横空出世。

Skyline 新渲染引擎

官方声称新引擎能够解决我们上面的吐槽:

在 Skyline 环境下,我们尝试改变这一情况:Skyline 创建了一条渲染线程来负责 Layout, Composite 和 Paint 等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。这种新的架构相比原有的 WebView 架构,有以下特点:

  • 界面更不容易被逻辑阻塞,进一步减少卡顿
  • 无需为每个页面新建一个 JS 引擎实例(WebView),减少了内存、时间开销
  • 框架可以在页面之间共享更多的资源,进一步减少运行时内存、时间开销
  • 框架的代码之间无需再通过 JSBridge 进行数据交换,减少了大量通信时间开销

而与此同时,这个新的架构能很好地保持和原有架构的兼容性,基于 WebView 环境的小程序代码基本上无需任何改动即可直接在新的架构下运行。

牛蛙~ 牛蛙~,由于我也是刚得知消息,新引擎体验报告待我后续奉上🐶。

其实本篇文章就是看到了新渲染引擎的消息后想写的,各位先当做个铺垫,有助于我们更好地去理解和使用新引擎。

最后,说了半天Webview,那 Webview 到底是个啥,这里再给大家解释一下。

WebView

WebView是术语,是指网页视图。可以内嵌在移动端,实现前端的混合式开发,大多数混合式开发框架都是基于WebView模式进行二次开发的。比如:APIcloud、uni-app等等的框架。

WebView是一个基于webkit引擎、展现web页面的控件,用于在手机系统中展示网页,可以简单的理解为一个可以嵌套到界面上的一个浏览器控件。

webkit是一个浏览器引擎,也就是浏览器内核,处理CSS、DOM、渲染等。

不同运行环境中,使用的WebView不同。 image.png

感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,点赞、关注、收藏,作者会持续与大家分享更多干货