小程序原理 及 优化

1,530 阅读5分钟

小程序使用的是双线程

在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 >

两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 NativeJSBrigde 做中转

小程序的启动过程

  • 1、小程序初始化: 微信初始化小程序环境:包括 Js 引擎WebView 进行初始化,并注入公共基础库。 这步是微信做的,在用户打开小程序之前就已经准备好了,是小程序运行环境预加载。
  • 2、下载小程序代码包 对小程序业务代码包进行下载:下载的不是小程序的源代码,而是编译、压缩、打包之后的代码。
  • 3、加载小程序代码包 对下载完成对代码包进行注入执行。 此时,app.js、页面所在的 Js 文件和所有其他被require 的 Js 文件会被自动执行一次,小程序基础库会完成所有页面的注册。
  • 4、初始化小程序首页 拉取数据,从逻辑层传递到视图层,进行渲染

setData 的工作原理

  • 1、调用setData方法;
  • 2、逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,将待传输数据转换成字符串并拼接到特定的JS脚本, 并通过 evaluateJavascript 执行脚本将数据传输到渲染层。
  • 3、渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。
  • 4、WebView 线程开始执行渲染时,将 data setData 数据套用在WXML 片段上,得到一个新节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。最后,将 setData 数据合并到 data 中,并用新节点树替换旧节点树,用于下一次重渲染

小程序官方性能指标

  • 1、首屏时间不超过 5 秒
  • 2、渲染时间不超过 500ms
  • 3、每秒调用 setData 的次数不超过 20 次
  • 4、setData 的数据在 JSON.stringify 后不超过 256kb
  • 5、页面 WXML 节点少于 1000 个,节点树深度少于 30 层子节点数不大于 60 个
  • 6、所有网络请求都在 1 秒内返回结果;

小程序优化

1、分包并且使用

分包预加载(通过配置 preloadRule) 将访问率低的页面放入子包里,按需加载;启动时需要访问的页面及其依赖的资源文件应放在主包中。 不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转;可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;

image.png

2、采用独立分包技术(感觉开普勒黄金流程源码可以独立分包)

主包+子包的方式,,如果要跳到子包里,还是会加载主包然后加载子包;采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;

3、异步请求可以在页面onLoad就加载

4、注意利用缓存

利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新

5、及时反馈

及时对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了 先反馈,再请求。比如说,点赞的按钮,可以先改变按钮的样式,再 发起异步请求。

6、可拆分的部分尽量使用自定义组件

自定义组件的更新并不会影响页面上其他元素的更新,各个组件具有独立的逻辑空间、数据、样式环境及 setData 调用

7、避免不当的使用onPageScroll

避免在onPageScroll 中执行复杂的逻辑,避免在onPageScroll中频繁使用setData,避免在onPageScroll中 频繁查询几点信息(selectQuery

8、减少在代码包中直接嵌入的资源文件;图片放在cdn,使用适当的图片格式

9、setData 优化

(1)与界面渲染无关的数据最好不要设置在 data 中,可以考虑设置在 page 对象的其他字段下;

this.setData({ 
    a: '与渲染有关的字符串', 
    b: '与渲染无关的字符串' 
}) 
// 可以优化为 
this.setData({ 
    a: '与渲染有关的字符串' 
}) 
this.b = '与渲染无关的字符串' 

(2)不要过于频繁调用 setData,将多次 setData 合并成一次 setData 调用

(3)数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示数据结构比较复杂包含长字符串,则不应使用setData来设置这些数据

(4)列表局部更新 在更新列表的某一个数据时。不要用 setData 进行全部数据的刷新。查找对应 id 的那条数据的下标(index是不会改变的),用 setData 进行局部刷新

this.setData({ 
    `list[${index}]` = newList[index] 
}) 

(5)切勿在后台页面进行setData(就是不要再页面跳转后使用setData) 页面跳转后,代码逻辑还在执行,此时多个webview是共享一个js进程;后台的setData操作会抢占前台页面的渲染资源;

10、避免过多的页面节点数

页面初始渲染时,渲染树的构建、计算节点几何信息以及绘制节点到屏幕的时间开销都跟页面节点数量成正相关关系,页面节点数量越多,渲染耗时越长。

每次执行 setData 更新视图,WebView JS 线程都要遍历节点树计算新旧节点数差异部分。当页面节点数量越多,计算的时间开销越大,减少节点树节点数量可以有效降低重渲染的时间开销。

11、事件使用不当

(1)去掉不必要的事件绑定(WXML中的bindcatch),从而减少通信的数据量和次数; (2)事件绑定时需要传输targetcurrentTargetdataset,因而不要在节点的data前缀属性中放置过大的数据

12、逻辑后移,精简业务逻辑

就比如咱们生成分享图片,再比如领取新人券的时候将是否是新人是否符合风控条件和最终领券封装为一个接口

13、数据预拉取(重要

小程序官方为开发者提供了一个在小程序冷启动时提前拉取第三方接口的能力 developers.weixin.qq.com/miniprogram… 预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度

14、跳转时预拉取

可以在发起跳转前(如 wx.navigateTo 调用前),提前请求下一个页面的主接口并存储在全局 Promise 对象中,待下个页面加载完成后从 Promise 对象中读取数据即可

15、非关键渲染数据延迟请求

小程序会发起一个聚合接口请求来获取主体模块的数据,而非主体模块的数据则从另一个接口获取,通过拆分的手段来降低主接口的调用时延,同时减少响应体的数据量,缩减网络传输时间。

16、分屏渲染

在 主体模块 的基础上再度划分出 首屏模块 和 非首屏模块(比如京挑好货的猜你喜欢模块),在所有首屏模块都渲染完成后才会渲染非首屏模块和非主体模块,以此确保首屏内容以最快速度呈现

17、接口聚合,请求合并(主要解决小程序中针对 API 调用次数的限制)

在小程序中针对 API 调用次数的限制: wx.request (HTTP 连接)的最大并发限制是 10 个; wx.connectSocket (WebSocket 连接)的最大并发限制是 5 个;

18、事件总线,替代组件间数据绑定的通信方式

通过事件总线(EventBus),也就是发布/订阅模式,来完成由父向子的数据传递

19、大图裁剪为多块加载

20、长列表优化

(1)不要每次加载更多的时候 都用concat 每获取到新一页数据时,就把它们concatlist上去,这样就会导致每次setData时的list越来越长越来越长,渲染速度也就越来越慢 (2)分批setData,减少一次setData的数量。不要一次性setData list,而是把每一页一批一批地set Data到这个list中去

this.setData({ 
    ['feedList[' + (page - 1) + ']']: newVal, 
}) 

(3)运用官方的 IntersectionObserver.relativeToViewport 将超出或者没进入可视区的 部分卸载掉(适用于一次加载很多的列表数据,超出了两屏高度所展示的内容)

image.png

this.extData.listItemContainer.relativeToViewport({ top: showNum * windowHeight, bottom: showNum * windowHeight }) 
    .observe(`#list-item-${this.data.skeletonId}`, (res) => { 
        let { intersectionRatio } = res 
        if (intersectionRatio === 0) { 
            console.log('【卸载】', this.data.skeletonId, '超过预定范围,从页面卸载') 
            this.setData({ 
                showSlot: false 
            }) 
        } else { 
            console.log('【进入】', this.data.skeletonId, '达到预定范围,渲染进页面')
            this.setData({ 
                showSlot: true, 
                height: res.boundingClientRect.height 
            }) 
        }
    }) 

21、合理运用函数的防抖与节流,防止出现重复点击及重复请求出现 为避免频繁setData和渲染,做了防抖函数,时间是600ms