启动流程
环境准备
- 小程序运行进程及运行环境的准备
- 代码包下载、校验及初始化
- 视图层系统组件、webview容器和原生组件的初始化
- 逻辑层JS引擎初始化及域创建
代码注入
- 框架及第三方基础代码的初始化 — 小程序基础库、扩展库、插件,自定义组件
- 开发者代码注入
- 开发者逻辑层代码 — 派发
App.onLaunch还有App.onShow
这些事件 - 开发者视图层代码 — 公共代码以及页面代码的注入
- 开发者逻辑层代码 — 派发
首屏渲染
- 逻辑层页面的初始化,这个时间点是initDataSendTime的一个触发时机,会派发Page.onLoad事件
- 视图层时间点走到viewLayerReaderStartTime,会派发Page.onShow事件
- 开发者代码从后端拉取数据,准备data数据
- 页面的整体的渲染
- 视图层时间点走到viewLayerReaderEndTime,会派发Page.onReady事件,标志着首屏渲染的完成
启动方式
冷启动
- 用户设备上第一次打开或销毁后再打开小程序,进入后台30分钟以后再次进入前台冷启动
热启动
- 小程序启动的一种优化机制,小程序进入后台30分钟以内再次进入前台,可以直接从后台状态然后恢复到前台,前面的启动流程都不会执行
小程序性能的优化
冷启动性能的优化
涉及的生命周期函数
App.onLaunch
: 监听小程序初始化 — 启动流程,一次性App.onHide
:监听小程序切后台 — 运行时性能App.onShow
:监听小程序启动或切前台 — 视图显示时派发,重复派发与启动流程相关的事件,运行时性能
涉及的页面生命周期函数
Page.onLoad
:监听页面加载 — 启动流程,一次性Page.onShow
:监听页面显示 — 视图显示时派发,重复派发与启动流程优化相关的一次性事件Page.onHide
:监听页面隐藏 — 运行时性能Page.onReady
:监听页面初次渲染完成 — 启动流程,一次性Page.onUnload
:监听页面卸载 — 运行时性能
顺着整个过程和涉及的生命周期函数的可优化节点
- 环境准备阶段、拉取小程序基本信息阶段 — 拉取信息是同步(用户使用越多启动性能越好,提高轮询机制的命中率)
- 紧跟小程序基础库更新 — 提高预加载环境的命中率
- 代码注入阶段 — 减少代码的注入量和复杂度,以期减少启动时间
- 合适的生命周期函数节点 — 首屏渲染要从后端拉取数据并在首页进行渲染时,使用异步转同步的编程范式以及使用并发复合命令,在多个文件里边对齐这个代码的执行点
Page.onReady
时间派发、首屏渲染完成阶段 — 使用动态数据加载的,使用骨架屏技巧、压缩图片、提高服务器接口响应效率和数据传输效率- 数据预拉去和周期性更新机制 — 微信提供了数据预加载周期性更新机制
- 低端机首次渲染需要较长时间的情况下 — 微信提供了初始渲染缓存机制
运行时渲染性能的优化
双线程运行机制
- 微信小程序可以看作是由逻辑层、视图层两个线程协同完成运行的
- 逻辑层负责执行JS代码,视图层负责渲染UI页面。逻辑层与视图层之间的事件触发以及数据传递,即setData方法的调用全是由底层的Native层负责中转完成的。
- setData函数用于更新视图数据,按照微信小程序的性能评判标准,setData每次传递的数据大小不能超过256KB,超过这个限制页面就容易卡顿。在页面或者是列表组件scroll事件里面,频繁地调用setData,视图层来不及渲染也会出现明显的卡顿现象。
冷启动时以及运行时可以使用的性能优化技巧
- 使用WXS脚本,在视图层完成事件处理
- 重渲染机制
- 支持
WXWebAssembly
- 允许开发者另开
Worker
线程 — 针对JS是单线程执行 - 分页渲染、使用虚拟
DOM
— 针对长列表页面setData单次传递的数据不能超过256KB限制 - 使用
LocalStorange
接口数据缓存于本地 — 针对每次拉取动态数据需要时间 - 5s的“挂起”状态 — 进入后台后有5秒挂起状态,在这种状态下setData没有必要执行
- 启用
Http2、Quic
协议 — 与后台进行数据交互的时候可以启用Http2 Quic等协议 getCurrentPages()
接口 — 在page.Unload中将定时器以及wx.onxx的全局监听和与全局对象有关的事件监听全部移除- 原生的
Context
节点 — 原生组件,可以通过SelectorQuery查询到这个原生节点,然后再利用这个原生节点直接操作和更新视图 - 本地图片上传到云
骨架屏
- 优缺点:不能使首屏渲染加快,但在白屏时给用户提供反馈,减缓用户焦虑等待的情绪
- 在data中默认设置loading:true,在数据加载完成后设置loading:false,在页面中可以通过wx:if 和 wx:else来避免数据先加载出来导致和骨架屏页面重叠
- 不要直接修改生成的骨架屏代码
- 修改配置以后要重新生成骨架屏代码,wxml或js代码后续有修改也要重新生成骨架屏
- 骨架屏只给主页使用
使用虚拟DOM,优化长列表内容
- 使用recycle-view组件(平台能力->扩展能力->扩展组件)
- npm install –save miniprogram-recycle-view 安装完成后,构建npm
- 在页面的json文件中将
recycle-view
和recycle-item
引用到usingComponent - 很好的实现了虚拟DMOM
- 结合滚动事件scrolltolower,还可以实现逐页加载与更新列表数据
- 本身已经默认开启throttle函数节流机制
- 预留了插槽,方便开发者添加个性业务
页面容器page-container
- 做弹窗
优化视图页动画效果
- 使用Animation对象实现的CSS动画 — 效率最低
- 使用页面或组件对象拥有的animate,实现
关键帧
动画 — 第一推荐,效率非最高 - 滚动事件驱动的响应式动画 — 推荐的低成本创建响应式页面效果的方式
- 本质上响应式动画也是通过animation接口实现的
- 依据scroller这个组件的滚动而变化
- 通过
wx.createSelectorQuery().select('#id').fields()
去查询scroller组件 - 然后再回调里面调用animate去设置它的对象,然后再onReady中调用
- 通过WXS脚本实现的样式动画 — 效率最高,适用高频动画,但目前只支持es5
- 由.wxs脚本实现,事件绑定再一个WXS脚本导出的事件句柄函数
- 通过事件句柄函数的ownerInstance调用的selectAllComponents或者selectComponent去查询页面上的组件,查询后通过setStyle设置组件的样式
重渲染与自定义组件优化
重渲染: 使用新的节点树,有目标地将原节点树上需要更新的节点一一更新的过程
- 将界面功能组件化 — 频繁改动的功能
- 去掉不必要的数据设置,减少每次setData设置的数据量
- 通过wxs脚本改写组件,可以在视图层完成的代码逻辑
- wxml增加了wxs模块的引入,并且在view中添加change:mode=”“ mode=”“,wxs脚本中通过函数判断mode来调用对应模式的函数或代码
- 使用page.requestAnimationFrame()可以防止跟不上渲染造成卡顿
代码按需注入与初始渲染缓存
- “lazyCodeLoading”: “requiredComponents”, 按需注入
- “initialRenderingCache”:”xxx”, 使视图层不需要等待逻辑层初始化完毕
- 参数:
static
静态初始渲染缓存 |dynamic
动态缓存 - this.setInitialRenderingCache({swiperlist: swipers}) 设置需要动态缓存的数据
- 参数:
- 静态导航页用初始渲染缓存,动态详情页用骨架屏
使用独立分包和分包预加载
- this.selectComponent(‘#id’).select(2)指定tarbar默认选中那个索引
- tarbar最好使用自定的组件
- 独立分包,获取
getApp({allowDefault : true})
1
2
3
4
5"preloadRule": {
"页面路径": {
"nrework": "all",
"packages": "__APP__" //代表主包
}}
独立分包使用占位组件 — 分包异步化
- 前提项目已经启用懒加载机制,代码是按需加载注入的,使用占位组件,给自定义组件安排一个替身,在真实的自定义组件加载并注入之前先用替身展示,进一步优化启动的性能
组件的分包异步化及主页中占位组件的使用
- 将首页使用的所有自定义组件移动到分包中,然后再使用占位组件延迟加载它们
- app.json创建组件分包,可以没有页面,把组件放在该分包中
- 修改主页配置,将组件由全局用用改为页面引用,并进行占位的组件声明,
- 在页面的
componentPlaceholder
选项下设置组件替身{“原组件名”:”自定义组件|标准组件view”}, 使用自定义组件的话要先引入,然后使用自定义组件名即可
使用封面页
- 创建一个空白效果的页面,作为伪首页只放一个logo和名称,在onReady中添加跳转到真首页代码,作用加载主包中的所有基础内容,加载完成后跳转真首页
- 封面页位于主包内,真首页移到分包中,同时preloadRule开启最该分包的预加载
非独立分包中的页面被访问时主包同时也会被下载,但其他普通分包不会被下载。
独立分包它并不加载主包,放在主包里边的共享代码,在独立分包里无法直接使用
JS代码的分包异步化
1 | ;(async () => {} |
- 在app.js文件中定义两个支持绝对路径的方法,代替默认的require以及require.async(少用)
项目插件化
- 在插件模式下,静态依赖分析工作不了
- 使用插件模式,创建plugin目录,在app.json中给使用的分包配置plugin选项,在app.json中添加pluginRoot选项,配置plugin的放置位置
- 在onReady中 requirePlugin(‘myPlugin’,plugin => {})
- 或 requirePlugin.async(“myPlugin”).then(plugin => {})
- 使用立即执行函数
1
2
3
4;(async () => {
requirePlugin('myPlugin',plugin => {})
// 或 let plugin = await requirePlugin.async("myPlugin").catch()
}){}
使用WXWebAssembly优化运算性能
允许开发者使用Go、C、C++等后端强类型语言编写代码,然后将其编译为一种类似于汇编代码的二进制代码,弥补解析新语言JS在执行性能上的不足
- 编写go语言 –> 编译压缩为wasm文件 –> 从Go语言源码里面拷贝并且修改wasm_exec.js文件(必不可少),同时也需要一个text_encoder.js文件
- 在项目的根目录创建一个目录,放置Go语言代码
- 先安装Go语言包(Go版本和文件想对应,所以版本安装要谨慎)
- 在调用前关闭环境变量 go env -w GO111MODULE=off
- 调用./build.sh (GO111MODULE=off GOOS-js GOARCH=wasm go build -o 编译后的名字.wasm 编译的文件.go)
- 压缩文件, 安装brew install brotli,目标位置删除 rm -f 目标路径/xx.wasm.br, brotli -o 文件的位置/xxx.wasm.br xxx.wasm
- 将相关的js文件、wasm文件全部拷贝至对应的目录,并在次目录下依照原组件的代码,创建新组件
使用异步转同步的编程范式
异步编程: 除了主线程以外还有一个或者多个异步线程,异步线程处理worker timer定时器、网络请求、用户输入监听、事件派发等任务,
当异步线程有回调函数代码需要执行的时候,异步代码将这些代码推入到主线程的执行队列里面去,由主线程在不是很忙碌的时候尽快将这些代码进行执行。
- 编写异步转同步函数:promisify替代request,文件放在公共的位置
- require不支持绝对路径,但在app.js文件中可以使用绝对路径
- 全局的全部示例app可以通过getApp()取到,app.js文件位于项目的根目录下方便标记
- 函数调用时为了避免程序报错,在后面一定要加上一个默认的catch设置。
- 使用promisify函数的父函数,由于添加了async关键字,已经是同步函数,要避免在主线程上以阻塞的方式,即添加await关键字的方式调用函数
使用复合命令模式对齐代码的执行点
复合命令模式是一个设计模式,它和异步转同步的编程范式一样主要作用在于使我们这个代码结构变得更加清晰,易于维护,其次它还可以优化代码的调用逻辑,统筹安排代码的一个执行时机。
- 在App.onLaunch事件在开始拉取动态数据,将App.onLaunch和首页的Page.onLoad这两个时间节点使用并发的复合命令模式管理起来,让它们可以并发执行
使用worker开启新线程进行耗时运算
worker:依托于寄主的环境而存在的,可以在后台并行执行JS代码,不影响页面渲染的技术,将worker看作异步线程,可以在异步线程中执行一些比较耗时的计算代码,在计算代码执行完成以后再将执行结果同步给主线程使用
- 创建worker线程
- 在app.json文件中
配置workers:workers"
- 在app.json文件中
- 创建组件,只有js改动,在使用后主动销毁worker
- this.worker=wx.createWorker(“目录位置”,{useExperimentalworker:true})
- worker和wxs都是异步执行worker比wxs的限制
- 不能使用wx开头的小程序接口
- workers目录下面可以有很多的worker文件,但同时只能有一个worker线程在开启,如果想要开新的线程,需要将原来的先给停掉,销毁掉,并且如果系统资源紧张,worker线程还有可能被系统回收掉
- worker只能放在特定的已经配置好的目录下面,不能随意地放置在其他目录下
- 通讯不方便,worker线程和主线程之间的通讯只能使用postMessage和onMessage进行相互通讯,这是相当于观察者模式的一种通讯机制
在后端使用Go语言异步执行逻辑运算代码(如何将前端工作后移)
- 创建后端计算接口,存放在server目录
- main.go 会有启动代码,在终端中使用npm dev,要先启用go语言模块化
- 扩展接口,将go代码的要扩展的接口主要的代码和相关变量,函数拷贝的项目中要使用页面的其他代码上方
- 用curl测试api地址curl api地址
使用串发命令模式延迟同步请求(如何使用数据缓存)
同步会阻塞主流线程
有些接口虽然名称上由Sync结尾,但实际却仍是同步接口,如wx.getSystemInfo、wx.getStorage、wx.setStorage这三个接口经常在App.onLaunch还有Page.onLoad中用到
用分接口获取系统信息
wx.getSystemSetting
获取设备设置wx.getAppAuthorizeSetting
获取微信APP授权设置wx.getDeviceInfo
获取设备基础信息wx.getWindowInfo
获取窗口信息wx.getAppBaseInfo
获取微信APP基础信息
启动过程先使用默认参数,启动完成后(即Page.onReady事件派发后)再进行相关接口的调用和缓存后端接口数据的本地缓存存取代码
创建SystemInfoManager模块,这样系统信息的获取在首屏渲染后
- global.asyncRetrieveSystemInfo.getCommand(0).markComplete()允许异步拉取系统信息
- 拉去系统信息的代码卸载onLaunch
- 分页拉取数据,默认page=1
首页动态数据的优化
- 在App.onLaunch节点开始加载数据
- 只加载首页首次渲染所需的一页数据
小程序切换后台后,关闭对setData的一个调用(监听App进入后台)
wx.onAppHide
监听小程序进入后台事件或者App.onHide
捕捉小程序进入后台的时机- 劫持Page对象,再page.js文件中先劫持onLoad和setData,在onLoad中获得setData的引用,然后在app.js文件里面调用page.js文件
- 定义工具函数mySetData,在原来所有调用setData的地方改为调用mySetData,不建议
- 如果必须要发生劫持的话一定要把劫持代码同意放在程序的入口处
在项目外使用数据预拉取与周期性更新(弱网情况)
- 数据预拉取:在小程序启动时,由微信异步调用开发者设置的原函数或数据接口,待拿到数据后,在传递给小程序使用
- 周期性更新:微信每隔12小时轮询开发者设置的云函数或数据接口,由开发者在小程序中取用
- 在微信小程序平台开启
- 使用并发复合命令的竞赛模式拉取数据
wx.onBackgroundFetchData
微信在拿到预拉取数据的时候设置一个回调函数wx.getBackgroundFetchData
主动获取微信在本地缓存的预拉取数据,本地没有则拉取失败
优化后端接口及网络请求参数(wx.request)
- enableCache,开启cache
- enableHttp2,开启http2 — 后端需要开启相关支持
- enableQuic,开启quic — 第三代网络,后端需要开启相关支持
视图代码优化技巧
在动态列表渲染优化
wx:key
使用- 如果列表元素是单一的基本数据类型,并且是唯一的,直接写成
*this
,*this就代表当前数据列表中的元素item - 如果列表元素是对象类型,可以填写列表元素对象中的一个字段名,这个字段名在整个数据列表中必须是唯一的
- 列表不是动态的,只渲染一次,wx:key设置为index
- 如果列表元素是单一的基本数据类型,并且是唯一的,直接写成
绑定视图事件
- 使用
catch
代替bind
,不需要冒泡只在特定节点监听事件 - 回传额外信息用
data-
的形式
- 使用
使用节流函数和防抖函数,防止按钮误单击与scroll事件函数频繁触发
- 节流:控制某段JS代码的执行频率
- 防抖:避免一次事件当作多次处理
- 对于scroll高频事件要节流,使用节流函数
throttle
- 对于用户的
单击
事件,可以适当使用防抖函数debounce
重渲染与使用wxml标签要克制
- 减少wxml节点的数量,总页面节点数少于1000个,节点数深度层级少于30层,子节点数不大于60个
- 控制setData每次传递的数据量
- 能不用容器标签就不用
WXSS优化技巧
- 给滚动组件开启惯性滚动
- 添加-webkit-overflow-scrolling: touch;样式
- 使用hover-class实现按钮的单击态
- 代替:active伪类
- 使用
gulp
工具删除无用wxss样式 — 只能静态甄别不能动态- npm install gulp -g
- npm install –save-dev gulp gulp-cleanwxss
- 创建一个gulpfile.js文件
- 执行 gulp
1
2
3
4
5
6
7
8// gulpfile.js文件
const gulp = require("gulp")
const cleanwxss = require("gulp-cleanwxss")
gulp.task("default", (done) => { // 默认执行
gulp.arc("../miniprogram/index/page/*/*.wxss") // 处理的文件
.pipe(cleanwxss({log: true}))
.pipe(gulp.dest("./dist")) //处理结果放在该目录下
})
UI交互技巧
- 使用padding改变单击区域大小
- 使用伪元素改变点击区域大小
- 给父元素reletive,伪类为absolute,且top、left、right、bottom设置为负值,扩大点击区域
脚本优化技巧
- 如果对象被异步线程引用,或者是被全局对象引用,会造成内存泄漏
- 定时器是异步线程里的东西,离开页面一定要记得销毁
- 使用wx.onXXX全局绑定一定要小心,有一个监听必须有一个反监听,在onUnload事件销毁
- 主题切换事件在模拟器可以通过在app.json配置”darkmode”:true
- 使用全局对象要小心
- 所有在global上或者在app上定义的全局数据,或者在上面添加的事件监听
- 要及时清理,自定义对象要定义一个dispose方法
- 使用this对象要谨慎(在周期性发生的异步回调函数里面)
setData调用优化
- 小程序切换到后台后,setData不再调用,可以结合小程序的双线程运行机制和重渲染机制
- 不要多次分开调用setData,尽量要合并调用
- 不准备渲染的数据不要放在data数据对象里边
- 只有需要触发视图更新的数据才需要放在data对象
- 不需要触发的,可以放在data对象外面(即当前页面对象上),通过this直接取用
- 通过index局部更新长列表数据
- 使用索引或计算属性更新局部数据
- 直接使用当前组件上下文对象的update方法
网络请求优化之使用本地缓存 并发请求的优先级不一致
减少不必要的网络请求,使用本地缓存的数据代替从后端接口拉取的数据
- 在首页的JS文件里边有加载小程序导航数据的代码,可以再这里尝试使用本地缓存技术
- 首先从本地缓存中尝试取出缓存数据,然后向后端发起网络请求,拿到最新的导航数据以后再调用setData重新设置一下数据,并把本地数据也刷新一遍,避免本地缓存过时(优化:改用并发复合命令或两个异步函数)
优化网络请求参数,提高网络请求的通讯效率 — 前面讲wx.request参数已讲
优化网络请求的并发数,让优先级高的请求先执行
- 因为有由wx.request发出的请求有最大10个的并发限制(限基础库1.4.0以下)
- 改造request工具函数
- 安装 yarn add priority-async-queue
- 构建npm
- 使用自定义的request方法时,在重要的网络请求添加priority选项
const res = await request({url:"",priority:"urgent"})
- 在自定义的request方法中引入
1
2
3
4
5
6
7
8
9
10
11const PriorityAsyncQueue = require "priority-async-queue"
const queue = new PriorityAsyncQueue(10) // default 10
const low="low",normal="normal",mid="mid",high="high",urgent="urgent"
export const priority = {low , normal, mid, high, urgent}
const priority = args.priority ? args.priority : normal
return new Promise((resolve,reject) => {
queue.addTask({prority}, ()=>{
wx.request(
Object.assign(args,{success:resolve,fail:reject})
)})
})
图片优化技巧
- 尽量减少图片的请求参数
- 尽量压缩图片的大小
- 尽量使用带有cdn加速的网络图片链接
- 尽可能使用高压缩比图片,例如webp格式的图片
- 生成雪碧图
- 多张图片合成一张图片,通过background-image来使用的图片
- npm install libpng
- npm -g install miniprogram-slim
- 执行generate_sprite.sh生成的脚本
- 图片压缩
- 在线 https://tinypng.com/
- 离线 将图片存放的文件夹拷贝在tools文件夹下,然后执行cpmpress.sh脚本
- npm install libpng
- miniprogram-slim imagemin -o ./生成的文件夹 –png-quality “0.65,0.8” ./存放图片的文件夹/**/*.png
- 使用腾讯云cos存储本地图片
- 上传本地图片到cos的方法
- uploadImageToCos()
- npm install cos-nodejs-sdk-v5
- 然后登录腾讯云官方网站开通对象存储服务并创建一个默认的存储桶
- 创建一个upload.js文件,实现uploadImageToCos函数
- 上传本地图片到cos的方法