京喜小程序自去年双十一上线微信购物一级入口后,时刻迎接着亿级用户量的挑战,细微的体验细节都有可能被无限放大,为此,“极致的页面性能”、“友好的产品体验” 和 “稳定的系统服务” 成为了咱们开发团队的最基本执行原则。javascript
首页做为小程序的门户,其性能表现和用户留存率息息相关。所以,咱们对京喜首页进行了一次全方位的升级改造,从加载、渲染和感知体验几大维度深挖小程序的性能可塑性。css
除此以外,京喜首页在微信小程序、H五、APP 三端都有落地场景,为了提升研发效率,咱们使用了 Taro 框架实现多端统一,所以下文中有部份内容是和 Taro 框架息息相关的。html
提起互联网应用性能这个词,不少人在脑海中的词法解析就是,“是否足够快?”,彷佛加载速度成为衡量系统性能的惟一指标。但这实际上是不够准确的,试想一下,若是一个小程序加载速度很是快,用户花费很短期就能看到页面的主体内容,但此时搜索框却没法输入内容,功能没法被流畅使用,用户可能就不会关心页面渲染有多快了。因此,咱们不该该单纯考虑速度指标而忽略用户的感知体验,而应该全方位衡量用户在使用过程当中能感知到的与应用加载相关的每一个节点。前端
谷歌为 Web 应用定义了以用户为中心的性能指标体系,每一个指标都与用户体验节点息息相关:java
体验git
指标github
页面可否正常访问?web
首次内容绘制 (First Contentful Paint, FCP)json
页面内容是否有用?小程序
首次有效绘制 (First Meaningful Paint, FMP)
页面功能是否可用?
可交互时间 (Time to Interactive, TTI)
其中,“是否有用?” 这个问题是很是主观的,对于不一样场景的系统可能会有彻底不同的回答,因此 FMP 是一个比较模糊的概念指标,不存在规范化的数值衡量。
小程序做为一个新的内容载体,衡量指标跟 Web 应用是很是相似的。对于大多数小程序而言,上述指标对应的含义为:
综上,咱们已基本肯定了高性能的概念指标,接下来就是如何利用数值指标来描绘性能表现。
小程序官方针对小程序性能表现制订了权威的数值指标,主要围绕 渲染表现、setData
数据量、元素节点数 和 网络请求延时 这几个维度来给予定义(下面只列出部分关键指标):
setData
的次数不超过 20 次;setData
的数据在 JSON.stringify
后不超过 256kb;详见 小程序性能评分规则
咱们应该把这一系列的官方指标做为小程序的性能及格线,不断地打磨和提高小程序的总体体验,下降用户流失率。另外,这些指标会直接做为小程序体验评分工具的性能评分规则(体验评分工具会根据这些规则的权重和求和公式计算出体验得分)。
咱们团队内部在官方性能指标的基础上,进一步浓缩优化指标系数,旨在对产品体验更高要求:
setData
的数据量不超过 100kb;小程序提供了 体验评分工具(Audits
面板) 来测量上述的指标数据,其集成在开发者工具中,在小程序运行时实时检查相关问题点,并为开发者给出优化建议。
以上截图均来自小程序官方文档
体验评分工具是目前检测小程序性能问题最直接有效的途径,咱们团队已经把体验评分做为页面/组件是否能达到精品门槛的重要考量手段之一。
咱们知道,体验评分工具是在本地运行小程序代码时进行分析,但性能数据每每须要在真实环境和大数据量下才更有说服力。恰巧,小程序管理平台 和 小程序助手 为开发者提供了大量的真实数据统计。其中,性能分析面板从 启动性能、运行性能 和 网络性能 这三个维度分析数据,开发者能够根据客户端系统、机型、网络环境和访问来源等条件作精细化分析,很是具备考量价值。
其中,启动总耗时 = 小程序环境初始化 + 代码包加载 + 代码执行 + 渲染耗时
不少时候,宏观的耗时统计对于性能瓶颈点分析每每是杯水车薪,做用甚少,咱们须要更细致地针对某个页面某些关键节点做测速统计,排查出暴露性能问题的代码区块,才能更有效地针对性优化。京喜小程序使用的是内部自研的测速系统,支持对地区、运营商、网络、客户端系统等多条件筛选,同时也支持数据可视化、同比分析数据等能力。京喜首页主要围绕 页面 onLoad
、onReady
、数据加载完成、首屏渲染完成、各业务组件首次渲染完成 等几个关键节点统计测速上报,旨在全链路监控性能表现。
另外,微信为开发者提供了 测速系统,也支持针对客户端系统、网络类型、用户地区等维度统计数据,有兴趣的能够尝试。
为了更好地为小程序制订性能优化措施,咱们有必要先了解小程序的底层架构,以及与 web 浏览器的差别性。
微信小程序是大前端跨平台技术的其中一种产物,与当下其余热门的技术 React Native、Weex、Flutter 等不一样,小程序的最终渲染载体依然是浏览器内核,而不是原生客户端。
而对于传统的网页来讲,UI 渲染和 JS 脚本是在同一个线程中执行,因此常常会出现 “阻塞” 行为。微信小程序基于性能的考虑,启用了双线程模型:
上图来自小程序官方开发指南
然而,任何线程间的数据传输都是有延时的,这意味着逻辑层和视图层间通讯是异步行为。除此以外,微信为小程序提供了不少客户端原生能力,在调用客户端原生能力的过程当中,微信主线程和小程序双线程之间也会发生通讯,这也是一种异步行为。这种异步延时的特性会使运行环境复杂化,稍不注意,就会产出效率低下的编码。
做为小程序开发者,咱们经常会被下面几个问题所困扰:
接下来,咱们会结合小程序的底层架构分析出这些问题的根本缘由,并针对性地给出解决方案。
小程序启动阶段,也就是以下图所示的展现加载界面的阶段。
在这个阶段中(包括启动先后的时机),微信会默默完成下面几项工做:
1. 准备运行环境:
在小程序启动前,微信会先启动双线程环境,并在线程中完成小程序基础库的初始化和预执行。
小程序基础库包括 WebView 基础库和 AppService 基础库,前者注入到视图层中,后者注入到逻辑层中,分别为所在层级提供其运行所需的基础框架能力。
2. 下载小程序代码包:
在小程序初次启动时,须要下载编译后的代码包到本地。若是启动了小程序分包,则只有主包的内容会被下载。另外,代码包会保留在缓存中,后续启动会优先读取缓存。
3. 加载小程序代码包:
小程序代码包下载好以后,会被加载到适当的线程中执行,基础库会完成全部页面的注册。
在此阶段,主包内的全部页面 JS 文件及其依赖文件都会被自动执行。在页面注册过程当中,基础库会调用页面 JS 文件的 Page 构造器方法,来记录页面的基础信息(包括初始数据、方法等)。
4. 初始化小程序首页:
在小程序代码包加载完以后,基础库会根据启动路径找到首页,根据首页的基础信息初始化一个页面实例,并把信息传递给视图层,视图层会结合 WXML 结构、WXSS 样式和初始数据来渲染界面。
综合考虑,为了节省小程序的“点点点”时间(小程序的启动动画是三个圆点循环跑马灯),除了给每位用户发一台高配 5G 手机并顺带提供千兆宽带网络以外,还能够尽可能 控制代码包大小,缩小代码包的下载时间。
通过屡次业务迭代,无可避免的会存在一些弃用的组件/页面,以及不被调用的函数、样式规则,这些冗余代码会白白占据宝贵的代码包空间。并且,目前小程序的打包会将工程下全部文件都打入代码包内,并无作依赖分析。
所以,咱们须要及时地剔除再也不使用的模块,以保证代码包空间利用率保持在较高水平。经过一些工具化手段能够有效地辅助完成这一工做。
在小程序中,全部页面的路径都须要在小程序代码根目录 app.json
中被声明,相似地,自定义组件也须要在页面配置文件 page.json
中被声明。另外,WXML、WXSS 和 JS 的模块化都须要特定的关键字来声明依赖引用关系。
WXML 中的 import
和 include
:
<!-- A.wxml --> <template name='A'> <text>{{text}}</text> </template> <!-- B.wxml --> <import src="A.wxml"/> <template is="A" data="{{text: 'B'}}"/> 复制代码
<!-- A.wxml --> <text> A </text> <!-- B.wxml --> <include src="A.wxml"/> <text> B </text> 复制代码
WXSS 中的 @import
:
@import './A.wxss' 复制代码
JS 中的 require
/import
:
const A = require('./A') 复制代码
因此,能够说小程序里的全部依赖模块都是有迹可循的,咱们只须要利用这些关键字信息递归查找,遍历出文件依赖树,而后把没用的模块剔除掉。
JS Tree-Shaking 的原理就是借助 Babel
把代码编译成抽象语法树(AST),经过 AST 获取到函数的调用关系,从而把未被调用的函数方法剔除掉。不过这须要依赖 ES module,而小程序最开始是遵循 CommonJS 规范的,这意味着是时候来一波“痛并快乐着”的改造了。
而 CSS 的 Tree-Shaking 能够利用 PurifyCSS 插件来完成。关于这两项技术,有兴趣的能够“谷歌一下”,这里就不铺开细讲了。
题外,京东的小程序团队已经把这一系列工程化能力集成在一套 CLI 工具中,有兴趣的能够看看这篇分享:小程序工程化探索。
小程序代码包最终会通过 GZIP 压缩放在 CDN 上,但 GZIP 压缩对于图片资源来讲效果很是低。如 JPG
、PNG
等格式文件,自己已经被压缩过了,再使用 GZIP 压缩有可能体积更大,得不偿失。因此,除了部分用于容错的图片必须放在代码包(譬如网络异常提示)以外,建议开发者把图片、视频等静态资源都放在 CDN 上。
须要注意,
Base64
格式本质上是长字符串,和 CDN 地址比起来也会更占空间。
这是一个 “痛并快乐着” 的优化措施。“痛” 是由于须要给后台同窗提改造需求,分分钟被打;“快乐” 则是由于享受删代码的过程,并且万一出 Bug 也不用背锅了...(开个玩笑)
经过让后台承担更多的业务逻辑,能够节省小程序前端代码量,同时线上问题还支持紧急修复,不须要经历小程序的提审、发布上线等繁琐过程。
总结得出,通常不涉及前端计算的展现类逻辑,均可以适当作后移。譬如京喜首页中的幕帘弹窗(以下图)逻辑,这里共有 10+ 种弹窗类型,之前的作法是前端从接口拉取 10+ 个不一样字段,根据优先级和 “是否已展现”(该状态存储在本地缓存) 来决定展现哪种,最后代码大概是这样的:
// 检查每种弹窗类型是否已展现 Promise.all([ check(popup_1), check(popup_2), // ... check(popup_n) ]).then(result => { // 优先级排序 const queue = [{ show: result.popup_1 data: data.popup_1 }, { show: result.popup_2 data: data.popup_2 }, // ... { show: result.popup_n data: data.popup_n }] }) 复制代码
逻辑后移以后,前端只需负责拿幕帘字段作展现就能够了,代码变成这样:
this.setData({ popup: data.popup }) 复制代码
京喜首页做为电商系统的门户,须要应对各种频繁的营销活动、升级改版等,同时也要知足不一样用户属性的界面个性化需求(俗称 “千人千面”)。如何既能减小为应对多样化场景而产生的代码量,又能够提高研发效率,成为燃眉之急。
相似于组件复用的理念,咱们须要提供更丰富的可配置能力,实现更高的代码复用度。参考小时候很喜欢玩的 “乐高” 积木玩具,咱们把首页模块的模板元素做颗粒度更细的划分,根据样式和功能抽象出一块块“积木”原料(称为插件元素)。当首页模块在处理接口数据时,会启动插件引擎逐个装载插件,最终输出个性化的模板样式,整个流程就比如堆积木。当后续产品/运营须要新增模板时,只要在插件库中挑选插件排列组合便可,不须要额外新增/修改组件内容,也更不会产生难以维护的 if
/ else
逻辑,so easy~
固然,要完成这样的插件化改造免不了几个先决条件:
下面为你们提供部分例程来辅助理解。其中,use
方法会接受各种处理钩子最终拼接出一个 Function
,在对应模块处理数据时会被调用。
// bi.helper.js /** * 插件引擎 * @param {function} options.formatName 标题处理钩子 * @param {function} options.validList 数据校验器钩子 */ const use = options => data => format(data) /** * 预置插件库 */ nameHelpers = { text: data => data.text, icon: data => data.icon } listHelpers = { single: list => list.slice(0, 1), double: list => list.slice(0, 2) } /** * “堆积木” */ export default { 1000: use({ formatName: nameHelpers.text, validList: listHelpers.single }), 1001: use({ formatName: nameHelpers.icon, validList: listHelpers.double }) } 复制代码
<!-- bi.wxml --> <!-- 各模板节点实现 --> <template name="renderName"> <view wx:if="{{type === 'text'}}"> text </view> <view wx:elif="{{type === 'icon'}}"> icon </view> </template> <view class="bi__name"> <template is="renderName" data="{{...data.name}"/> </view> 复制代码
// bi.js Component({ ready() { // 根据 tpl 值选择解析函数 const formatData = helper[data.tpl] this.setData({ data: formatData(data) }) } }) 复制代码
小程序启动时只会下载主包/独立分包,启用分包能够有效减小下载时间。(独立)分包须要遵循一些原则,详细的能够看官方文档:
小程序提供了 web-view 组件,支持在小程序环境内访问网页。当实在没法在小程序代码包中腾出多余空间时,能够考虑降级方案 —— 把部分页面 h5 化。
小程序和 h5 的通讯能够经过 JSSDK 或 postMessage 通道来实现,详见 小程序开发文档。
白屏阶段,是指小程序代码包下载完(也就是启动界面结束)以后,页面完成首屏渲染的这一阶段,也就是 FMP (首次有效绘制)。
FMP 无法用标准化的指标定义,但对于大部分小程序来讲,页面首屏展现的内容都须要依赖服务端的接口数据,那么影响白屏加载时间的主要由这两个元素构成:
小程序提供了读写本地缓存的接口,数据存储在设备硬盘上。因为本地 I/O 读写(毫秒级)会比网络请求(秒级)要快不少,因此在用户访问页面时,能够优先从缓存中取上一次接口调用成功的数据来渲染视图,待网络请求成功后再覆盖最新数据从新渲染。除此以外,缓存数据还能够做为兜底数据,避免出现接口请求失败时页面空窗,一石二鸟。
但并不是全部场景都适合缓存策略,譬如对数据即时性要求很是高的场景(如抢购入口)来讲,展现老数据可能会引起一些问题。
小程序默认会按照 不一样小程序、不一样微信用户 这两个维度对缓存空间进行隔离。诸如京喜小程序首页也采用了缓存策略,会进一步按照 数据版本号、用户属性 来对缓存进行再隔离,避免信息误展现。