小程序 性能优化(如何打造高性能小程序门户网站)

作者:阿伟【凹凸实验室】转发链接:https://aotu.io/notes/2020/03/25/high-performance-miniprogram/
后台小程序在第11个小时迎接每年上亿用户的挑战,细微的体验细节可能被无限放大。正因如此,“极致的页面性能”、“友好的产品体验”、“稳定的系统服务”成为我们开发团队最基本的实现原则。
首页作为小程序的入口,其性能与用户留存率息息相关。因此,我们对京西首页进行了全面升级,从加载、渲染、感知体验几个维度深挖小程序的性能可塑性。
此外,京西首页在微信小程序、H5、APP上都有落地场景。为了提高研发能力;d效率,我们已经用Taro框架实现了多端统一,所以下面的一些内容和Taro框架密切相关。
如何定义高绩效?一提到互联网应用性能这个词,很多人脑海里的词法分析就是“够快吗?”似乎加载速度是衡量系统性能的唯一标准。但这其实不够准确。试想一下,如果一个小程序加载速度非常快,用户需要很短的时间才能看到页面的主要内容,但此时搜索框无法输入内容,功能无法流畅使用,用户可能并不关心页面渲染的速度有多快。因此,不能单纯考虑速度指标而忽略用户的感知体验,而应该综合衡量用户在使用过程中所能感知到的与应用加载相关的每一个节点。
为Google Web应用定义了一个以用户为中心的性能指标体系,每个指标都与用户体验节点紧密相关:
体验指标页面可以正常访问吗?第一个内容丰富的油漆,FCP)页面内容有用吗?第一个有意义的画图,FMP)页面功能可用吗?互动时间,TTI)
其中“有用吗?”这个问题很主观,不同场景下的系统可能会有完全不同的答案,所以FMP是一个比较模糊的概念指标,没有标准化的数值度量。
小程序作为一种新的内容载体,在衡量指标上与Web应用非常相似。对于大多数小程序来说,上述指标对应的含义是:
FCP:白屏加载完成;FMP:第一屏渲染完成;TTI:所有内容都已加载;综上所述,我们基本确定了高绩效的概念指标,接下来就是如何用数值指标来描述绩效了。
小程序官方性能指标。小程序官方针对小程序的性能制定了数值指标,主要围绕渲染性能、setData数据量、元素节点数、网络请求延迟等维度进行定义(以下仅列出部分关键指标):
首屏时间不超过5秒;渲染时间小于500ms;SetData每秒调用不超过20次;SetData的数据不超过256kbJSON.stringify之后;页面上WXML节点少于1000个,节点树深度少于30层,子节点数不超过60个;所有网络请求在1秒内返回结果;有关详细信息,请参见applet性能评分规则。
我们要把这一系列官方指标作为小程序的性能及格线,不断打磨和提升小程序的整体体验,降低用户流失率。此外,这些指标还会直接作为小程序体验评分工具的绩效评分规则(体验评分工具会根据这些规则的权重和求和公式计算出体验得分)。
在官方性能指标的基础上,我们团队针对产品体验的更高要求,进一步集中优化了指标系数:
首屏时间不超过2.5秒;setData的数据量不超过100kb;所有网络请求在1秒内返回结果;滑动组件和滚动长列表时没有卡壳的感觉;体验评分工具小程序提供了一个体验评分工具(审计面板)来衡量上述指标数据,它被集成到开发人员的工具中。小程序运行时可以实时检查相关问题,并给开发者提出优化建议。
以上截图均来自小程序官方文档。
目前,经验评分工具是检测小程序性能问题最直接有效的方法。我们团队已经把经验评分作为考量页面/组件是否能达到优质产品门槛的重要手段之一。
小程序后台性能分析。我们知道经验评分工具是用来分析小程序在本地运行时的情况,但是性能数据往往需要在真实环境和大数据量下更有说服力。无独有偶,小程序管理平台和小程序助手为开发者提供了大量真实的数据统计。其中,性能分析面板从启动性能、运行性能、网络性能三个维度分析数据。开发者可以根据客户端系统、型号、网络环境、访问来源等条件进行详细分析,极具考虑价值。
其中,总启动时间=小程序环境初始化代码包加载代码的渲染时间。
很多时候,第三方测速系统的宏观耗时统计对于性能瓶颈分析往往是杯水车薪,作用不大。我们需要对一个页面的一些关键节点进行更详细的测速统计,找出暴露性能问题的代码块,从而更有效地进行优化。京西小程序采用内部自研测速系统,支持区域、运营商、网络、客户端系统等多条件筛选。还支持数据可视化、同比数据分析等能力。北京首页主要针对页面onLoad、onReady、数据加载完成、首屏渲染完成、各业务组件首次渲染完成等几个关键节点进行统计和速度报告,旨在监控整个链路的性能。
此外,微信为开发者提供了测速系统,还支持客户端系统、网络类型、用户地域等维度统计。有兴趣的可以试试。
了解小程序的底层架构为了更好地制定小程序的性能优化措施,我们有必要先了解小程序的底层架构,以及小程序与web浏览器的区别。
微信小程序是大前端跨平台技术的产物之一。与React Native、Weex、Flutter等其他热门技术不同,applet最终的渲染载体仍然是浏览器内核,而不是native client。
对于传统的网页,UI渲染和JS脚本是在同一个线程中执行的,所以经常会出现“阻塞”行为。基于性能考虑,微信小程序启用了双线程模式:
视图:即webview线程,负责使不同的webview呈现不同的小程序页面;逻辑层:单独的线程执行JS代码,可以控制视图层的逻辑;上图来自小程序官方开发指南。
然而,任何线程之间的数据传输都是延迟的,这意味着逻辑层和视图层之间的通信是异步的。除此之外,微信为小程序提供了很多客户端原生能力。在调用客户端原生能力的过程中,微信的主线程和小程序的双线程也会进行通信,这也是一种异步行为。这种异步延迟会使运行环境复杂化,如果不注意就会产生低效代码。
作为applet开发者,我们经常被以下问题困扰:
小程序启动慢;白屏时间长;页面呈现速度慢;运行内存不足;接下来我们将结合小程序底层架构分析这些问题的根源,并给出针对性的解决方案。
小程序启动太慢?小程序启动阶段,即显示如下图所示加载界面的阶段。
在这个阶段(包括启动前后的计时),微信会默默完成以下任务:
1.准备运行环境:
小程序启动前,微信会先启动双线程环境,在线程中完成小程序基础库的初始化和预执行。
小程序基础库包括WebView基础库和AppService基础库。前者注入视图层,后者注入逻辑层,分别为各个层提供其运行所需的基本框架能力。
2.下载applet代码包:
小程序第一次启动时,编译好的代码包需要下载到本地。如果启动小程序分包,只会下载主包的内容。另外,代码包会保留在缓存中,后续启动会优先读取缓存。
3.加载applet代码包:
小程序代码包下载完成后,会加载到合适的线程中执行,基本库会完成所有页面的注册。
在这个阶段,主包中的所有页面JS文件及其依赖文件都将被自动执行。
在页面注册过程中,基本库会调用页面JS文件的页面构造函数方法,记录页面的基本信息(包括初始数据、方法等。).
4.初始化小程序的主页:
小程序代码包加载后,基础库会根据启动路径找到主页,根据主页的基本信息初始化一个页面实例,并将信息传递给视图层,视图层会结合WXML结构、WXSS样式和初始数据来渲染界面。
综合考虑,为了节省小程序的“一点点时间”(小程序的启动动画是三个圆形打勾),除了给每个用户送一部5G手机,顺带提供千兆宽带网络,还可以尽量控制代码包的大小,缩短代码包的下载时间。
不必要的文件、函数和样式被删除。经过多次业务迭代,不可避免的会有一些被抛弃的组件/页面,以及没有使用的功能和样式规则。这些多余的代码会白白占用宝贵的代码包空间。而且目前小程序的打包会把项目下的所有文件都驱动到代码包里,没有依赖分析。
因此,我们需要及时淘汰未使用的模块,以确保代码包空间的利用率保持在较高的水平。一些工具性的手段可以有效地辅助这项工作。
小程序中的文件依赖分析,所有页面的路径都需要在小程序代码的根app.json中声明。同样,自定义组件也需要在页面配置文件page.json中声明另外,WXML、WXSS、JS的模块化都需要特定的关键字来声明依赖和引用关系。
导入并包含在wx中:
{{text}}
@在bwxss中导入:
@ import ‘。/require/@import ‘。/A.wxss’JS:
Const A=require(‘。/A ‘)。所以,可以说小程序中所有的依赖模块都是可追溯的。我们只需要利用这些关键字信息递归地找到它们,遍历文件依赖树,然后剔除无用的模块。
JS,CSS Tree-ShakingJS Tree-Shaking的原理是借助巴别塔将代码编译成抽象语法树(AST),通过AST得到函数的调用关系,从而剔除未被调用的函数方法。但是,这需要依靠ES模块,小程序最早遵循了CommonJS规范,意味着是时候来一波“痛并快乐着”的改造了。
CSS的树摇动可以通过PurifyCSS插件来完成。关于这两项技术,有兴趣的可以“谷歌一下”,这里就不赘述了。
另外JD。COM的小程序团队已经将这一系列工程功能集成到一套CLI工具中。有兴趣的话,请看这篇分享:小程序工程探索。
减少代码包中的静态资源文件。小程序代码包最终会通过GZIP压缩放到CDN上,但是GZIP压缩对图像资源的作用很低。比如JPG、PNG等格式的文件早就压缩好了,再用GZIP压缩可能会让体积变大,得不偿失。所以建议开发者把图片、视频等静态资源放在CDN上,除了一些用于容错的图片必须放在代码包里(比如网络异常提示)。
请注意,Base64格式本质上是一个长字符串,它将比CDN地址占用更多的空间。
逻辑后移,业务逻辑简化。这是一个“痛并快乐着”的优化措施。“痛”是因为要找后台同学转型,分分钟被打;“开心”是因为你很享受删除代码的过程,万一出了Bug也不用背黑锅……(开个玩笑)
通过让后台承担更多的业务逻辑,可以节省小程序的前端代码量。同时,在线问题也支持紧急修复,无需再经历小程序试用、发布、上线的繁琐过程。
得出的结论是,一般不涉及前端计算的展现逻辑可以适当后移。比如京西首页的窗帘弹窗逻辑(如下图),这里有10种弹窗类型。之前的做法是前端从接口拉取10个不同的字段,根据优先级和“是否已经显示”来决定显示哪一个(这个状态保存在本地缓存)。最后,代码是这样的:
//检查每个弹出类型是否都显示了promise.all ([check (popup _ 1),check (popup _ 2),//.check (popup _ n)]。然后(result={//优先级顺序const queue=[{show3360result . popup _ 1 data 3360 data . popup _ 1 },{ show 3360 result。popup _ 2data3360data。popup _ 2},//.{ show 3360结果。popup _ nda3360data。popup _ n}})逻辑后移,前端只需要显示幕布场,代码变成这样:
this . setdata({ popup 3360 data . popup })重用模板插件京西首页作为电商系统的入口,需要处理各种频繁的营销活动、升级改版等。同时还需要满足不同用户属性的个性化需求(俗称“千人千面”)。如何减少为应对不同场景而生成的代码量,并提高R & amp效率已经成为一种迫切的需要。
类似于组件重用的概念,我们需要提供更丰富的可配置能力来实现更高的代码重用。参考我小时候喜欢玩的乐高积木玩具,我们把家用模块的模板元素划分成更细的粒度,按照样式和功能抽象出一块块“积木”原料(称为插件元素)。主页模块在处理界面数据时,会启动插件引擎逐个加载插件,最终输出个性化的模板样式。整个过程就像堆木头一样。后续产品/操作需要添加模板时,只需在插件库中选择插件进行排列组合即可。不需要添加/修改额外的组件内容,也不会出现难以维护的if/else逻辑,好轻松~ ~
当然,完成这样的插件转换有几个先决条件:
用户体验设计的统一。如果设计风格总是不一样,强行外挂只会成为负担。服务接口的统一。同样,如果你不得不浪费大量的精力去兼容不同模块之间的接口场差异,那将是非常痛苦的。这里有一些套路可以帮助你理解。其中,use方法会接受各种处理钩子,最后拼接一个函数,在对应模块处理数据时调用。
//bi . helper . js/* * * plugin engine * @ param { function } options . format name头处理hook * @ param { function } options . valid list数据检查器hook */Constuse=options=data=format(data)/* *预设插件库*/name helpers={ text 3360 data=data . text,icon : data=data . icon } list helpers={ single : list=list . slice(0,1),double: list=list.slice(0,2)}/** * ”
text icon//bi . js component({ ready(){//Select解析函数const format data=helper[data . TPL]this . setdata({ data : format data(data)} })分包加载程序启动时只会下载主包/独立分包。启用分包可以有效减少下载时间。(独立)分包需要遵循一些原则。详见官方文件:
web视图组件由h5 applet提供,它独立地分包部分页面,支持在applet环境中访问网页。当实在无法在小程序代码包中腾出额外空间时,可以考虑降级方案——到h5部分页面。
applet和h5之间的通信可以通过JSSDK或者postMessage通道实现。详情见applet开发文档。
白屏时间太长?白屏阶段是指小程序代码包下载完成后,页面第一次屏幕渲染完成的阶段(即启动界面完成),这是FMP(第一次有效渲染)。
FMP无法用标准化的指标来定义,但对于大多数小程序来说,页面首屏显示的内容取决于服务器的界面数据,所以白屏加载时间主要受这两个要素影响:
网络资源加载时间;渲染时间;启用本地缓存小程序提供了读写本地缓存的接口,数据存储在设备的硬盘上。由于本地I/O读写(毫秒级)会比网络请求(秒级)快很多,所以用户在访问页面时,可以先从缓存中取上一次成功接口调用的数据来渲染视图,然后覆盖最新的数据,在网络请求成功后重新渲染。另外,缓存的数据也可以作为底层数据,避免接口请求失败时一举两得。
然而,并不是所有的场景都适合缓存策略。例如,对于需要非常高的数据即时性的场景(比如抢购门户),显示旧数据可能会导致一些问题。
默认情况下,小程序会根据不同小程序和不同微信用户两个维度隔离缓存空间。比如京西小程序首页也采用了缓存策略,会根据数据版本号和用户属性进一步重新隔离缓存,避免信息虚假显示。
数据预取小程序官方为开发者提供了小程序冷启动时提前拉取第三方接口的能力:数据预取。
冷启动和热启动的定义可以在这里找到。
数据预取的原理其实很简单,就是在小程序启动时,微信服务器向第三方服务器发起HTTP请求获取数据,并将响应数据存储在本地客户端,供小程序前端检索。小程序加载时,只需调用微信提供的API wx.getBackgroundFetchData,即可从本地缓存中获取数据。这种方式可以充分利用小程序启动和初始化阶段的等待时间,使页面渲染更快完成。
这种能力已经在京西小程序首页的制作环境中实践过了。从每天数千万的数据分析中发现,预取将冷启动时获取接口数据的时间节点从2.5s加速到1s(提速60%)。虽然提升效果非常明显,但是这个能力还是有不成熟的地方:
预取的数据将被强缓存;由于预拉请求最终由微信服务器发起,或许由于服务器资源限制,预拉数据会在微信本地缓存一段时间,缓存失败后再重新发起请求。经过真机测试,预取缓存在微信购物入口冷启动京西小程序的场景下存活了30多分钟,这对于实时数据要求高的系统来说是非常致命的。请求者和响应者都不能被拦截;由于请求的第三方服务器是从微信的服务器发起的,而不是从小程序客户端发起的,本地代理无法拦截这种真实的请求,会导致开发者无法通过拦截请求来区分在线环境和开发环境的数据,给开发调试带来麻烦。小程序内部接口的响应体类型为all application/octet-stream,即数据格式未知,导致本地代理无法正确解析。微信服务器发起的请求,不提供区分在线版和开发版的参数,不提供用户IP等信息;如果这些问题都不会影响你的场景,可以尝试开启预取能力,对于小程序首屏的渲染速度是一个质的提升。
为了尽快获得服务器数据,通常会在触发页面onLoad挂钩时发起网络请求,但这并不是最快的方式。在从发起页面跳转到下一个页面onLoad的过程中,小程序需要完成一些环境初始化和页面实例化,大约需要300 ~ 400毫秒。
其实我们可以在发起跳转之前(比如调用wx.navigateTo之前)预先请求下一页的主接口并存储在全局Promise对象中,然后在加载下一页之后从Promise对象中读取数据。
这也是双线程模型的优点之一,它与多页面web应用的不同之处在于,当页面跳转/刷新时,窗口对象被销毁。
预下载如果启用了分包加载功能,当用户访问分包中的页面时,小程序将开始下载相应的分包。在分包下载阶段,页面会一直保持“白屏”启动状态,用户体验很差。
好在小程序提供了分包和预下载的能力,开发者可以在进入某个页面时配置可能用于预下载的分包,避免在页面切换时卡在“白屏”状态。
非关键渲染数据延迟请求这是关键渲染路径优化的思路之一,从缩短网络请求延迟的角度加快首屏渲染的完成时间。
关键渲染路径是指完成首屏渲染过程中必须发生的事件。
以京西小程序为例,每个模块都可能有大量的后台服务支持,这些后台服务之间的通信和数据交互会有一定的时间延迟。根据京西首页的页面结构,我们将所有模块分为两类:主模块(导航、商品转盘、商品豆腐块等。)和非主模块(窗帘弹窗、右挂件等。).
在初始化主页时,小程序会发起聚合接口请求,获取主模块的数据,而非主模块的数据则从另一个接口获取。通过拆分,会降低主接口的调用延迟,减少应答器的数据量,缩短网络传输时间。
分屏渲染也是关键的渲染路径优化思路之一。通过延迟非关键元素的呈现机会,资源可用于关键呈现路径。
和前面的措施类似,继续以京西小程序首页为例。在主模块的基础上,我们再次划分了第一屏模块(商品豆腐块上方)和非第一屏模块(商品豆腐块下方)。小程序在获取主模块的数据时,会优先渲染首屏模块,在所有首屏模块渲染完毕后,才会渲染非首屏模块和非主模块,以保证首屏内容以最快的速度呈现。
为了更好的呈现效果,上面的gif做了降速处理。
接口聚合,请求合并在小程序中,网络请求通过wx.request API发起。我们知道,在web浏览器中,对同一个域名的HTTP并发请求是有数量限制的;小程序中也有类似的限制,不同的是不是针对域名限制,而是针对API调用:
wx.request (HTTP连接)的最大并发限制是10;wx.connectSocket (WebSocket连接)的最大并发限制是5;超过并发限制的HTTP请求将被阻塞,它们需要在队列中等待前面的请求完成,从而在一定程度上增加了请求延迟。所以对于职责相近的网络请求,最好采用节流的方式,在一定的时间间隔内收集数据,然后合并成一个请求体发送给服务器。
图像资源优化图像资源一直是移动终端系统抢占大流量的部分,尤其是对于电商系统。优化图片资源的加载,可以有效加快页面响应时间,加快首屏渲染。
使用WebP格式WebP是Google推出的一种图像文件格式,支持有损/无损压缩。得益于更好的图像数据压缩算法,在裸眼无差别画质的前提下拥有更小的图像体积(根据官方说明,WebP的无损压缩体积比PNG小26%,有损压缩体积比JPEG小25-34%)。
图像组件的小程序支持JPG,PNG,SVG,WEBP,GIF等格式。
图像裁剪和退化由于移动设备的分辨率有限,很多图像的大小往往远大于页面元素的大小,这是对网络资源的浪费(一般情况下,图像大小为页面元素真实大小的两倍较为合适)。由于JD.COM强大的图像处理服务,我们可以通过资源的命名规则和请求参数获得服务器的优化图像:
裁剪成100×100的图片:3359 { host }/s100x 100 _ jfs/{ file _ path };
首付70%:https://{ href }!q70
惰性图像加载和CSS Sprite优化都是老套的图像优化技术,这里就不赘述了。
小程序的镜像组件自带lazy-load懒加载支持。CSS Sprite技术,请参考w3schools的教程。
在必须使用大图资源的场景中降级加载大图资源,可以适当使用“以经验换速度”的措施来提高渲染性能。
小程序会将加载的静态资源缓存在本地,短时间内再次发出请求时,会直接从缓存中取出资源(与浏览器行为一致)。所以对于大的图像资源,我们可以先呈现高度压缩的模糊图像,同时用一个隐藏节点加载原图,原图加载后再转移到真实节点进行渲染。整个过程中,从模糊到高清的过程会在视觉上有所感知,但相对于首屏渲染的推广效果,这种体验差距还是可以接受的。
以下是给你的一些套路:
//banner . js component({ ready(){ this . original URL=’ 3359 path/to/picture ‘//image源地址this . setdata({ URL 3360 compress(this . original URL))//加载压缩降质图片预加载URL : this . origin URL//预加载原图}}},方法3360 { onimglad(){ this . setdata({ URL 3360 this . origin URL//加载原图}}}}}注意带有display样式的标签:
北京首页的商品轮播模块也采用了这种降级加载方案,渲染首屏时只会加载第一张降级图片。基于每帧原始图像20~50kb的大小,该措施在初始阶段可以节省数百kb的网络资源请求。
为了更好的呈现效果,上面的gif做了降速处理。
一方面,我们可以从两个角度来缩短完成FMP(第一次有效渲染)的时间:减少网络请求延迟和关键渲染节点的数量。另一方面,我们也需要从用户感知的角度来优化加载体验。
“白屏”的加载体验对于初次使用的用户来说是无法接受的。我们可以使用尺寸稳定的骨架屏来帮助实现真正的模块占用和即时加载。
目前业内普遍采用骨架屏。京西首页选择灰色豆腐块作为骨架画面的主要元素,大致勾勒出各个模块主要内容的风格布局。由于微信小程序不支持SSR(服务器端渲染),动态渲染骨架屏的方案难以实现,所以京西首页骨架屏采用WXSS风格静态渲染。
有趣的是,京西首页的骨架屏方案经历了“统一管理”和“(组件)独立管理”两个阶段。为了避免组件的侵入,原来的骨架屏幕由一个完整的骨架屏幕组件管理:
主页面,但是这种方式的维护成本比较高。每次页面主模块更新迭代时,骨架屏组件中对应的节点都需要同步更新(比如调整某个模块的大小)。况且从骨架屏到真实模块的感官切换是一个飞跃,因为骨架屏组件和页面体节点的关系是互斥的。只有当页面主体数据准备就绪(或呈现)时,框架屏幕组件才会被销毁,主要内容才会呈现(或显示)。
为了让用户的感知体验更加流畅,我们将骨架屏幕元素拆分成各个业务组件,骨架屏幕元素的显示/隐藏逻辑由业务组件独立管理,可以轻松实现“谁跑得快,谁先出来”的并行加载效果。此外,框架屏幕元素与业务组件共享一组WXML节点,相关的样式由公共sass模块集中管理。业务组件只需要在适当的节点上挂skeleton和skeleton__block样式的块,大大降低了维护成本。
//banner . scss . banner-skeleton { @ include skeleton;banner _ wrapper { @ include skeleton _ _ block;}}}以上gif在压缩过程中出现了一些小问题。可以直接访问【京西】小程序体验骨架屏效果。
如何提高渲染性能?当调用wx.navigateTo打开新的applet页面时,applet框架将完成以下步骤:
1.准备新的webview线程环境,包括基础库的初始化;
2.从逻辑层到视图层的初始数据通信;
3.视图层根据逻辑层的数据和WXML片段构建节点树(包括节点属性、事件绑定等信息),最后结合WXSS完成页面渲染;
因为微信会提前开始准备webview线程环境,所以小程序的渲染损耗主要在于后两者的数据通信和节点树创建/更新过程。相应的,更有效的渲染性能优化方向是:
降低线程间的通信频率;减少线程间通信的数据量;减少WXML节点的数量;合并setData调用将多个setData调用尽可能合并为一个。
除了从编码标准上践行这个原则,我们还可以通过一些技术手段降低setData的调用频率。例如,当同一时间片(事件循环)中的setData调用合并在一起时,Taro框架就使用了这种优化方法。
在Taro框架下,调用setState时提供的对象会被添加到一个数组中,在执行下一个事件循环时这些对象会合并在一起,然后通过setData传递给原生applet。
//小程序中的时间片API const next tick=wx . next tick wx . next tick : settime out;不难发现,data中只放了与界面渲染相关的数据。setData传输的数据越多,线程间的通信时间越长,渲染速度越慢。根据微信官方测算的数据,传输时间和数据量一般是正相关的:
上图来自小程序官方开发指南。
所以与视图层渲染无关的数据尽量不要放在数据中,可以放在page (component)类的其他字段下。
每次应用层的data diff调用setData更新数据,都会导致视图层重新渲染。小程序会结合新的数据和WXML片段构建新的节点树,并与当前节点树进行比较,最终得到需要更新的节点(属性)。
即使applet在底层框架级别对节点树更新进行了区分,我们仍然可以优化这种区分的性能。比如在调用setData的时候,要确保所有传递的新数据都是提前更改的,也就是提前对数据做一个diff。
这一层优化是在Taro框架内部做的。Taro每次调用原生小程序的setData之前,都会做一个最新状态和当前页面实例的数据的diff,过滤掉需要更新的数据然后执行setData。
Taro框架的数据差异规则
去除不必要的事件绑定。当用户事件(如点击和触摸事件)被触发时,视图层会将事件信息反馈给逻辑层,这也是一个线程间通信的过程。但是,如果事件的回调函数没有在逻辑层绑定,则不会触发通信。
所以尽量减少不必要的事件绑定,尤其是onPageScroll等会被频繁触发的用户事件,会使通信过程频繁发生。
删除不必要的节点属性。组件节点支持额外的自定义数据集(参见下面的示例)。当用户事件被触发时,视图层会将事件目标和数据集数据传输到逻辑层。然后,自定义数据量越大,事件通信的时间就越长,所以应该避免在自定义数据中设置过多的数据。
点击我!//jspage({ bindviewtap(e){ console . log(e . current target . dataset)} })合适的组件粒度小程序的组件模型与Web组件标准中的ShadowDOM非常相似。每个组件都有独立的节点树,有自己独立的逻辑空间(包括独立数据、setData调用、createSelectorQuery执行域等。).
不难得出一个结论,如果自定义组件的粒度太粗,组件的逻辑太重,就会影响节点树构建的效率和新/旧节点树diff,从而影响组件中setData的性能。此外,如果在组件中使用createSelectorQuery查找节点,过大的节点树结构也会影响搜索效率。
我们来看一个场景。“JD。京西首页的COM秒杀”模块涉及一个倒计时功能,通过setInterval每秒调用setData更新拨号时间。通过从基本组件中提取倒计时,我们可以有效地减少频繁setData对性能的影响。
适当的组件化不仅可以减少数据更新的影响范围,还可以支持重用。为什么不呢?诚然,组件的粒度越细越好。组件的数量与小程序代码包的大小正相关。尤其是使用编译框架的项目(比如Taro),每个组件都会产生额外的运行时代码和环境polyfill,所以,为了代码包空间,请理性…
事件,而不是组件之间数据绑定的通信模式。WXML数据绑定是小程序中父组件向子组件传递动态数据的常用方式,如以下例程所示:组件A中的变量A和B通过组件属性传递给组件B。在这个过程中,不可避免的要经历一次组件A组件的setData调用来完成任务,这样就会导致线程之间的通信。“合理”,但是如果传递给子组件的数据只有一部分是与视图渲染相关的呢?
//Component b Component({ properties : { propa : String,propB: String,},Methods : { onload 3360 function(){ this . data . propathis . data . propb } })推荐一种在特定场景下非常方便的方法:通过EventBus完成父到子的数据传输,即发布/订阅模式。它的组成非常简单(例程只提供关键代码…):
一个全局事件调度中心类event bus { constructor(){ this . events={ } } on(key,CB) {this.events [key]。push (CB)} trigger (key,args) { this.events[key]。forEach(function(CB){ CB . call(this,args)} } remove(){ } } const event=new event bus()事件订阅者//子组件component({ created(){ event . on(‘ data-ready ‘,(data)={this。setdata ({data})}})事件发布者//parent component({ ready(){ Event。Trigger (‘data-ready ‘,data)}})子组件在创建时提前监听数据发送事件,当父组件获取数据时,触发事件将数据传输到子组件。这整个过程
但是并不是所有的场景都适合这种方法。对于京西首页这样一个具有“单向数据传输”和“显示交互”特点,有大量一级子组件的场景,使用事件总线的收益会非常高;但是,在频繁“双向数据流”的情况下,这种方式将很难维护交错事件。
题外话,Taro框架在处理父子组件之间的数据传输时,使用的是观察者模式。通过Object.defineProperty绑定父子组件关系,当父组件的数据发生变化时,会递归通知所有后代检查并更新数据。这个通知过程会同步触发数据diff和一些验证逻辑,每个组件运行一次大概需要5 ~ 10 ms。所以如果分量量级比较大,整个过程的时间损失也不小,还是可以尝试事件总线方案。
组件级别的差异。我们可能会遇到这样的需求。多个组件位置不固定,支持随时随地灵活配置。京西首页也有类似的需求。
北京主页的主体可以分为几个业务组件(如搜索框、导航栏、商品转盘等。),而且这些业务组件的顺序并不固定。今天搜索框在顶部,明天可能就变成顶部的导航栏了(夸张…)。我们不能为多个连续的可能性提供多组实现,所以我们需要使用自定义的小程序模板。
实现一个支持调度所有业务组件的模板,根据后台分布的模块数组循环渲染模板,如下面的例程所示。
//search-bar . js component({ properties : { floor id : Number,},created() { event.on(‘data-ready ‘,(comps)={ const data=comps[this . data . floor id]//根据楼层位置获取数据}}})看似很容易完成需求,但值得思考的是:如果调整组件的顺序,所有组件的生命周期会发生什么变化?
让我们假设上次呈现的组件顺序是[‘搜索栏’,’导航栏’,’横幅’,’图标导航’]。现在我们需要删除导航栏组件,并将其调整为[‘搜索栏’,’横幅’,’图标导航’]。实验表明,当一个组件节点发生变化时,其前面的组件不会受到影响,其后面的组件会被销毁并重新挂载。
原理很简单。每个组件都有自己孤立的ShadowTree,页体也是一个节点树。在调整组件顺序时,applet框架会遍历并比较新/旧节点树的差异,因此发现新节点树的导航条组件节点缺失,认为该(树)分支下的导航条节点发生了变化,所有未来节点都需要重新渲染。
但实际上这里的组件顺序是不变的,丢失的组件应该不会影响其他组件的正常渲染。所以我们在setData之前做了新旧组件列表的区分:如果newList中的组件是oldList的子集,并且相对顺序没有改变,那么所有组件都不会被重新挂载。此外,我们还要在接口数据的相应位置填入开销数据并隐藏组件,完成。
借助component diff,可以有效降低视图层的渲染压力。如果有类似场景的朋友,也可以参考这个方案。
内存太大?想必没有什么比小程序崩溃更影响用户体验了。
当小程序占用系统资源过多时,可能会被系统破坏,也可能会被微信客户端主动回收。应对这种尴尬局面,除了提示用户提高硬件性能(比如在JD.COM商城买新手机),还可以通过一系列的优化方法来减少小程序的内存损耗。
内存告警小程序提供了一个API:wx.onMemoryWarning来监控内存不足的告警事件,旨在让开发者在收到告警时能够及时释放内存资源,避免小程序崩溃。但是对于小程序开发者来说,内存资源目前还不能直接接触。他们最多是调用wx.reLaunch清理所有页面堆栈,重新加载当前页面,以减少内存负荷(这个方案太粗暴了,不要冲动,想想就好……)。
但是,收集内存告警信息是有意义的。我们可以上报内存告警信息(包括页面路径、客户端版本、终端手机型号等。)到日志系统,分析哪些页面崩溃率较高,从而有针对性的进行优化,降低页面复杂度。
按照双线程模型,小程序的每个页面都会有一个独立的webview线程,但逻辑层是单线程的,即所有webview线程共用一个JS线程。这样当页面切换到后台状态时,仍然可以抢占逻辑层的资源,比如setInterval和setTimeout timer没有被破坏:
//page page({ onload(){ let I=0 setinterval(()={ I },100)}})即使是小程序的组件,在页面进入后台状态时也会继续旋转。
正确的方法是在页面隐藏时手动清除计时器,然后在必要时恢复onShow阶段的计时器。坦白说,一个定时器回调函数的执行对系统的影响应该是可以忽略的,但不能忽略的是回调函数中的代码逻辑。比如在定时器回调中保存大量的setData就很不舒服…
避免频繁事件中的繁重内存操作。我们经常会遇到这样的需求:广告曝光、图片懒加载、导航栏顶吸等。这些都需要我们在页面滚动事件被触发时,实时监控元素位置或者更新视图。了解了小程序的双线程模型后,我们不难发现,页面滚动时onPageScroll被频繁触发,会造成逻辑层和视图层的不断通信。如果此时“火上浇油”地调用setData传输大量数据,会导致内存利用率快速上升,从而使页面卡顿甚至“假死”。因此,我们最好遵循以下原则来监控频繁事件:
OnPageScroll事件回调使用节流;避免CPU密集型操作,如复杂计算;避免调用setData,或者减少setData的数据量;尽量用IntersectionObserver代替SelectorQuery,前者对性能影响较小;大长列表的优化根据小程序官方文档,大图长列表图片会造成iOS中WKWebView的回收,导致小程序崩溃。
对于较大的图像资源(比如全屏gif图像),我们只能尽量对图像进行降级或裁剪,但最好不要使用。
对于一个长列表,比如瀑布流,这里有一个思路:我们可以使用IntersectionObserver来监控长列表中组件与窗口的相交状态,当组件距离窗口超过某个临界点时,销毁组件来释放内存空间,用同样大小的骨架图来占坑;当距离小于临界点时,获取缓存的数据并重新加载组件。
然而,不可避免的是,当用户快速滚动长长的列表时,被破坏的组件可能无法及时加载,视觉上会出现短暂的白屏。我们可以适当调整破坏阈值,或者优化骨架图的样式,尽可能提升体验。
小程序官方提供了一个长列表组件,可以通过npm包引入。有兴趣的可以试试。
结合以上方法论,对京西小程序首页进行了全方位升级,并给出了答卷:
1.审计绩效得分审计工具86;
2.优化的首屏渲染完成时间(FMP):
3.优化前后的测速数据对比:
但随着业务迭代的不断发展,用户场景的日益多样,性能优化将成为我们日常开发中挥之不去的原则和主题。基于微信小程序开发中的性能相关问题,基于小程序的底层框架原理,探讨提升小程序性能体验的可能性,希望能给小程序带来参考价值。
推荐网页优化文章《手把手教你如何处理Web站点图片优化》
《全面优化Web站点页面加载速度策略》
《手把手教你通过图片优化,将网站大小减少一半以上》
《日访问百万级微信小程序优化技巧总结》
作者:阿伟【凹凸实验室】转发链接:https://aotu.io/notes/2020/03/25/high-performance-miniprogram/

好玩下载

疯狂填字3下载,疯狂填字下载6

2023-9-4 9:39:08

综合资源

八月图片,如何来形容八月

2024-1-22 13:43:33

购物车
优惠劵
搜索