- 了解小程序的启动流程(下)
[视频] 首先我们先了解一下微信小程序双线程运行机制。 微信小程序可以看作是由逻辑层、视图层两个线程协同完成运行的。 逻辑层负责执行JS代码,视图层负责渲染UI页面。逻辑层与视图层之间的事件触发以及数据传递,也就是setData的方法的一个调用全是由底层的Native层负责中转完成的。 我们以iOS Mac端为例,底层有一个叫做evaluateJavaScript函数,这个函数专门负责执行JS函数,每当这个逻辑层它有代码要执行的时候,这个代码它先转为字符串传递给这个函数,再由这个函数负责将这个代码传递给对应的WebView组件完成渲染、完成执行。 setData函数它用于更新视图数据,它的执行也不例外,底层的Native层,在逻辑层与视图层中间它像一座非常窄的 一个窄窄的一个独木桥,极大地限制了两个线程之间的一个通讯效率,按照微信小程序的性能评判标准,setData每次传递的数据大小不能超过256KB,超过这个限制 页面就容易卡顿,还有并不是低于256KB它便不会产生性能问题。在这个页面或者是列表组件scroll事件里面,如果我们频繁地调用setData视图层它来不及渲染也会出现明显的一个卡顿现象。 针对小程序的运行时特点有以下这些优化点,我们一起看一下。 首先是使用WXS的脚本,在这个视图层完成这个事件处理。 WXS脚本是微信自研的一种类似于JS的一个脚本语言,它完全可以在视图层线程里边独立完成执行,不需要由逻辑层进行插手。更无需底层进行中转,小程序使用重渲染机制。WXML节点越少、嵌套层次越浅、渲染效率越高,按照这个小程序的性能评判标准,每页总节点数要小于1000个,层次要低于30层,每个节点的子节点不能多于60个,针对JS脚本解析执行这样一个效率低下这样的一个特点。 小程序它支持WXWebAssembly这样的一个技术,可以将C、Go语言代码编译为wasm文件,在这个小程序中加载并直接调用,借用其他编译型语言的一个强大执行能力,来弥补解析性语言的性能上的一个不足。针对JS是单线程执行的一个特点,小程序还允许开发者另开一个Worker线程并发进行代码的一个运算,针对长列表页面setData单次传递的数据不能超过256KB限制可以采用分页渲染,采用虚拟DOM等优化技巧。这些技巧它不是小程序专用的,在HTML5 Web开发里面也用到了,但是Web开发它没有setData大小的限制,一般也不需要这么来做。 针对每次拉取动态数据需要时间可以使用LocalStorage接口,将这个数据缓存在本地,下次渲染的时候我们直接从本地去取。不过LocalStorage接口,无论是同步接口还是异步接口,它本质上其实都属于同步接口,在使用的时候我们要避免影响小程序的主体启动流程,小程序在进入后台状态以后它有5秒的一个挂起状态,在这种状态之下setData它已经没有必要执行了,这个时候执行其实就是对用户CPU资源的一个浪费。在这种情况下我们可以停止setData的代码的执行,在小程序与后台进行数据交互的时候可以启用Http2 Quic等协议,以提高网络通讯效率。 小程序的页面在跳转后,页面对象并没有立即销毁,通过getCurrentPages()接口仍然可以获取所有页面栈对象,在Page.onUnload周期函数里面,我们应该将定时器以及以wx.onXxx这样形式开头的全局监听、还有与全局对象有关的事件监听等等全部移除,避免GC不能及时进行垃圾内存的一个回收。 小程序里面还有一些原生组件例如像map video canvas等这些原生组件,它们都有原生的Context的节点,在JS代码中可以通过SelectorQuery查询到这个原生节点,然后再利用这个原生节点直接操作和更新视图,可以绕过底层Native层的一个数据中转,这个技巧可以提升原生组件的一个渲染效率。小程序中遇到的一些图片所有本地图片都可以传到云上转为网络图片,那图片还可以使用webp格式,小程序的image组件是支持这种图片格式的。 以上就是小程序在冷启动时以及运行时可以使用的性能优化技巧。微信开发者工具提供了静态代码分析、Performance面板、Memory面板、性能评分面板也就是Audits面板还有代码质量扫描工具等等这些工具可以对小程序代码进行分析,帮助开发者进行启动性能优化和运行时渲染性能优化。下面我们看项目诊断。 为小程序项目做项目诊断有很多工具,例如入口在这个项目详情面板里面的代码依赖分析性能报告,代码质量扫描以及调试区的Performance面板 Memory面板还有这个JavaScript Profiler面板和体验评分面板等等这些都是。下面我们依次看一下这些工具的运行效果,它们显示了我们的初始项目究竟有多么糟糕,这正是我们接下来课程里面要优化的一些地方。 首先看代码依赖分析。主包竟然有36MB,主包超过了2MB就无法上传了,主包必须进行优化,无依赖的文件总数也达到了124,最大的一张高清商品图片占用了11MB的一个大小,这是非常恐怖的,所有的商品图片都需要进行网络链接化不能放在本地,接下来我们看代码质量扫描。我们可以看到有多项指标都没有达标,每个红色提示部分都值得我们仔细对待、认真优化还有Performance面板中出现多个红三角,有的任务执行时间竟然超过了400ms 性能已经十分堪忧了。这是Memory面板的一个截图,小程序平稳运行的时候,貌似存在大量新增的内存数据,程序可能存在内存泄露的一个隐患,这些问题也是我们在优化的时候应该重点对待的。 接下来我们再看JavaScript Profiler面板的一个表现。我们可以看到个别脚本执行时间稍微长了一些,达到了200ms以上,这是性能评分面板的一个截图,综合体验评分整体上较差。最大节点数已经超过了1000个的最大限制,这是小程序在冷启动时调试区的一个截图,小程序的冷启动耗时竟然达到了7秒以上。这是在微信开发者工具上的一个耗时,在手机设备尤其是在低端机设备上可能这个时间还要更长,以上我们从诊断结果来看,目前小程序项目的启动效率和运行时性能都很不乐观。 下面我们看一下优化目标。小程序用户设备,按性能可以分为高、中、低三档,一般低端机相对高端机在启动性能上有2到3倍的一个差距,在优化的时候我们不仅要关注高端机的一个性能,还要更加关注低端机的一个表现。一个页面从单击到打开如果超过3秒,用户便很容易失去耐心,对于高端机这个启动要求还要更加放低一些, 开发的时候如果我们使用苹果iPhone8进行测试的话,启动的时候这个时间则要控制在2.5秒以内同时不能有明显的白屏现象,小程序页面操作不能有明显的卡顿现象,为了避免不必要的一些麻烦、达到一致的实践效果,你在安装软件的时候最好选择使用与我相同的软件版本,特别是对于这个Go语言如果这个版本不对的话,稍后我们在编译WebAssembly文件的时候,可能会遇到额外的一些麻烦,这节课我们就讲到这里。 这节课我们主要从整体上了解了小程序的启动流程及双线程的运行机制,简单阐述了小程序在启动流程中的一些优化要点以及运行时的一个渲染优化要点,所有的优化技巧都是建立在小程序的启动流程和运行机制这个基础之上的,熟练知晓这些启动流程和运行机制才能更好地理解和使用这些优化技巧。 从下节课开始我们就以我们糟糕的初始项目,以它为基础,然后开始我们优化技巧的一个学习与实践。 这里有一个问题请你思考一下: 骨架屏它是一个在首屏渲染阶段,可以使用的优化技巧,简单又容易操作。那么你知道小程序里边怎么样使用这个骨架屏技巧吗? 下节课我们就一起来深入探讨一下这个问题。
2022-07-13 - wepy脏检查原理、具体实现和实际应用
前言 为什么要做脏检查? 小程序是双线程的,它的渲染线程和脚本线程是分开的,意味着小程序页面不会像普通网页脚本运行时间过长导致阻塞页面的问题。但是两个线程并不是毫不相干,小程序页面的渲染需要的动态数据在脚本线程,这时候两个线程就需要进行通信来动态更新页面展示的数据,所以小程序提供setData的接口来帮助两个线程进行通信,这也是小程序调用的最频繁的接口。 为什么频繁调用setData会有问题?因为两个线程之间传递的数据是字符串的形式,收到传递过来的数据需要把字符串转为脚本执行,当频繁setData的时候,渲染线程一直在编译执行渲染,导致视图的用户操作不能及时传到逻辑层,看起来页面就会像卡顿一样响应变慢。同样,setData大量数据也会造成同样的问题,因为setData大量数据意味着编译执行的时间变长,视图更新变慢。 wepy使用脏检查对setData进行封装一是不需要手动去进行setData数据更新页面,更方便地修改数据;另外,wepy的脏检查会在函数运行周期结束后调用,即便在函数内多次进行数据修改最后也只会执行一次setData,不需要担心多次setData带来的性能问题。 wepy脏检查原理及其实现 了解了setData的原理后,来了解wepy的脏检查原理以及wepy是如何实现脏检查的。首先看一幅官方提供的执行脏数据检查的流程图。 [图片] wepy脏数据检查流程解读 首先wepy通过$$phase标记当前是否在进行脏数据检查,以此保证只有一个脏数据检查流程在运行。这个时候如果有另外的脏检查调用则把$$phase赋值为'$apply',不执行脏数据检查流程。脏检查流程结束后会根据$$phase === '$apply'判断是否存在下一个脏数据检查流程,存在则再次进行脏数据检查流程。 wepy脏数据检查过程一开始会把$$phase赋值为'$digest'标记当前正在进行脏数据检查状态,然后遍历数据列表的所有数据进行新旧值比对,将新旧值不相等的新值放到readyToSet对象中,然后使用readyToSet对象进行setData渲染页面,最后通过$phase判断是否还存在脏数据检查流程,存在则再次进行脏数据检查。 wepy脏数据检查源码 $apply的封装:脏检查的调用 [图片] $digest的封装:脏检查的流程 流程准备: [图片] 检查过程: computed对象的脏值检查: [图片] 上面这里可以看到每次执行脏检查流程遍历computed对象属性都会执行val = fn2.call(this),意味着每次脏检查流程总是会执行一遍computed对象属性里面的方法,这里也对应到下面wepy官网上面的一段话。所以避免在computed里面做耗时长的计算,以免加长脏检查流程的时间,延缓setData渲染视图更新。 data对象的脏值检查 watch监听处理: [图片] 上面可以看到的是脏检查流程中对watch的处理,第一个this.watch[k].call(this, this[k], originData[k]) 可以理解是调用watch对应属性的监听方法,第二个是表示watch对应属性的值不是一个函数而是一个字符串的时候,会去看methods是否存在同名属性的方法,存在则调用methods的同名方法,这个处理看起来就是watch和methods方法公用,示例代码如下。 [代码] watch = { A: 'String' } methods = { A(newValue, oldValue) { } } [代码] 组件props数据处理: [图片] 父子组件双向绑定的数据的处理: [图片] 这里主要是看脏检查流程是在父组件触发还是子组件触发,判断数据更新的来源,然后根据数据的更新来源来进行父子组件双向绑定数据的同步更新。 流程结束: [图片] wepy如何调用脏检查? 在文章的开头讲到wepy会在函数运行周期结束后调用,那么wepy到底是怎样调用的呢? 1.wepy对生命周期都进行了一层封装,在函数的后面调用了page.$apply()执行脏检查 [图片] 2.wepy封装了$bindEvt方法,对非onLoad、onShow等生命周期事件进行了进一步封装,详细看wepy源码base.js文件 总结 由于wepy脏检查是在函数运行周期结束时调用的,所以在函数内使用异步函数promise或者async/await的时候需要自己手动的调用$apply()执行脏检查,否则函数运行周期结束后调用$apply()此时异步函数还没有执行完毕。总的来说,wepy脏检查在做的两件事就是检测脏值和同步更新数据。 参考文章和网站: 小程序官方优化建议https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html wepy官网-wepy数据绑定方式 https://tencent.github.io/wepy/document.html#/
2019-06-11