指标:
- 页面是否能正常访问?首次内容绘制(First Contentful Paint, FCP)
- 页面内容是否有用?首次有效绘制(First Meaningful Paint,FMP)
- 页面功能是否可用?可交互时间(Time to Interactive,TTI)
页面内容是否有用比较主观。难以规范化数值衡量。
对于小程序
- 首次内容绘制 FCP = 白屏加载结束 = 页面开始展示的时间点 - 开始请求时间点(performance.Timing.navigationStart)。在 H5 中就是开始 head 的末尾插入 script,它的执行作为页面开始展示时间节点。在小程序里可以用 onLoad 生命周期回调触发时间替代吧?
- 首次有效绘制 FMP = 首屏渲染完成 = 首屏幕最慢的图片加载完成时间(或者无图片就 performance.timing.domContentLoadedEventStart)- performance.Timing.navigationStart。
- 可交互时间 TTI = performance.timing.domContentLoadedEventStart(页面元素加载完毕,图片未必加载完,但可以滚动,搜索操作)时间- performance.Timing.navigationStart。
小程序官方性能指标:
- 首屏时间不超过 5 秒
- 渲染时间不超过 500 ms
- 每秒调用 setData 的次数不超过 20 次
- setData 的数据再 JSON.stringify 后不超过 256 kb
- 页面 WXML 的节点少于 1000 个,节点树深度少于 30 层,子节点数不大于 60 个。
- 所有网络请求都在 1s 内返回结果;
小程序的最终渲染载体仍旧是浏览器内核,而非原生客户端。但给予性能考虑,它启用了双线程模型。
webview 线程负责视图渲染,逻辑层负责执行 JS 代码。逻辑层通过通过 setData 来传输给视图层数据渲染。然而任何线程间数据传递都有延时,所以通信是异步的,视图层没法控制具体的 DOM。
小程序加载流程
- 准备运行环境。启动双线程环境,预先加载小程序基础库(包括 Webview 的基础库和 AppService 的基础库,前者注入到试图中,后者注入到逻辑层)。
- 下载小程序代码包。初次启动需要下载编译后的代码包到本地,之后就会缓存代码。小程序代码表最终是经过 GZIP 压缩放在 CDN 上的。
- 加载小程序代码包。下载完后执行代码包,此阶段内主包内所有页面 JS 都会被执行。并做页面注册,会调用 JS 里的 Page 构造器来初始化 JS 的数据、方法。
- 初始化小程序首页。代码包加载完毕,基础库寻找到首页,初始化页面实例,并传递信息给视图层。视图层结合 WXML、WXSS,初始数据来渲染页面。
若是首次内容绘制 FCP 时间长,怎么优化?
这通常都是加载的代码包大引起的,则减小代码体积
- 减少小程序代码包体积,剔除掉冗余代码,可以使用打包工具做 tree-shaking(需要创建 webpack 化的小程序项目)。还有像是 moment 有替代方案,去掉这个也能让小程序变小。再开启 GZIP 压缩。
- 分包预下载。小程序提供的能力,可以在进入某个页面预下载接下来可能会用到的分包,避免切页面白屏。
- 部分页面 H5 化。小程序提供了 web-view 组件,可以直接引用外部 H5 页,一方面这也能减少小程序包的体积,一方面让经常需要高度变化的内容扩展更改的更便捷。
若是首次有效绘制 FMP 时间长,怎么优化?
骨架屏
- 上骨架屏吧,这是缩短 FMP 首屏渲染时间的重要手段,可以减缓用户的焦虑感。
图片优化
- 减少静态资源文件的大小,使用 webp,合适的图片压缩工具,合理的剪裁降质来减少图片体积
- CDN 图床加快图片的获取速度。
- 降级加载大图资源。对于真的需要一张渲染很大的一张图片,可以预先使用高度压缩模糊的图片占位替代(给一个量化标准是 200kb以内),等到原图加载完毕再转移到真实节点上渲染。只需要一个
display: none
的 image 标签,就可以做到只加载图片资源,但不渲染。
接口拉取优化
- 接口本地缓存;开启数据预拉取,小程序启动时,微信服务器可以代理小程序客户端获取 HTTP 请求,当小程序加载完后调用 wx.getBackgroundFetchData 从本地缓存拿数据。
- 跳转时预拉取数据。从上个页面跳转到下一个页面,环境初始化以及页面实例化的工作需要耗时 300~400 ms,所以可以不在 onLoad 钩子触发再发起网络请求,取而代之可以在上一个页面 wx.navigateTo 调用前获取接口,并存储在全局 Promise 对象中,等待下一个页面加载完成再从 Promise 对象中读取数据。这也是双线程模型的优势,不同页面跳转不需要销毁全局对象。
- 非关键渲染数据延迟请求。
- 请求合并。wx.request(HTTP连接)的最大并发数量是 10 个,请求频繁的时候可以检查是否可以合并一段时间内的请求数据,做一个请求发送给服务器。
若是可交互时间 TTI 长,怎么优化
wx.navigateTo 跳转时
- 准备新的 webview 线程环境,初始化基础库
- 从逻辑层到视图层初始数据通信
- 视图层结合 WXML、WXSS,初始数据来渲染页面。
切换时主要性能损耗在 数据通信 和 节点数创建/更新 上
- 降低线程通信频次
- 减少通信数据量
- 减少 WXML 节点数量。
- 减少事件数量及触发次数
做法有
- 合并 setData 调用
- 只把与渲染渲染相关的数据放到 data 中。
- 应用层数据 diff。只要调用 setData 那就必然会有视图层渲染(当然视图层自己也会合并 data 数据另找时间渲染),那么开发者自己可以比对两次数据是否有变化,有变化再 setState
- 去掉不必要的事件绑定。事件信息也需要从视图层通信反馈给逻辑层,所以尽量减少不必要的事件绑定,尤其是 onPageScroll 里面还 setData 的这种。
- 适当的组件颗粒度。嵌套层级不能太深,组件数量和小程序代码包大小正相关。但也不能往一个组件上挂载太多的东西。
内存占用高?怎么优化
内存占用高,内存空间会被系统销毁,或者被微信客户端主动回收,导致小程序 Crash
小程序提供了监听内存不足告警的事件 API:wx.onMemoryWarning,可以告知开发者内存紧张。但开发者无法操作内存资源,顶多调用 wx.reLaunch 来清理页面栈,重新加载页面,来降低内存负荷。但开发者更应该仔细取收集告警信息上报到日志系统,分析并对程序做优化。
- 回收后台页面计时器。从上一个页面切换到下一个页面,上一个页面的定时器 setTimeout 和 setTimeInterval 仍旧会继续执行,可能就会占用过多的内存资源,onHide 时清理掉,onShow 时再恢复。比如说小程序的
<swiper>
组件,进入后台时仍旧继续轮播。 - 避免频发事件中重度内存操作。比如说导航栏吸顶效果,onPageScroll 事件回调必须使用节流函数,并在它的回调中避免使用 setData。部分场景尽量使用 IntersectionObserver API。
- 大图,长列表优化。快速滚动长列表,被销毁的组件可能来不及加载完。小程序提供了长列表组件,virtual List