- 了解小程序的启动流程(上)
[视频] *所有课程源码的链接:https://gitee.com/geektime-geekbang_admin/weapp_optimize 你好,我是李艺,是腾讯云TVP 、小程序从0到1的作者,极客时间 微信小程序全栈开发实战课程讲师。 小程序上线已经5年时间了,日活目前已经达到了4.5亿+,已然成为任何一家互联网企业都不能忽视的产品运营阵地。小程序的开发能力经过微信团队数年来的不断地努力,目前已经日臻完善,它早已经不是那个随随便便翻翻文档就可以完成开发的一个简易框架了,尤其是在性能优化方面,会优化与不会优化 ,对于产品的运行效果可以说有霄壤之别。 首先我们看第一部分,需要明确一下有哪些需要优化的现象。不知道你的小程序产品有没有遇到过这样的一些问题,点开小程序一直都是白屏什么也看不到、Loading加载提示转了好几圈页面还不显示,单击页面链接的时候页面跳转迟钝迟迟打不开、有的按钮单击了好几次一点反应也没有,长列表内容在滑动的时候越往下滑 页面越卡顿。你有没有因为这些问题而遭受过用户的抱怨,你会以为这些问题都是因为平台技术不完善而造成的吗? 那么为什么京东 滴滴等大厂的一些小程序,它们的功能那么繁杂。但使用起来却还是那么流畅呢,当我们抱怨框架不够给力的时候,我们对微信小程序的性能优化技巧又真正了解和使用多少?性能优化它是一个现人现地的活,讲究具体问题具体分析,需要有一个字节 一个字节去抠,一个毫秒 一个毫秒去节省的这样的一个细致精神。这个课程我们会演示相关的性能优化技巧。我准备了一个性能比较堪忧的项目,这个课程我们就一起诊断 优化这个项目,让它从体态臃肿的一个状态慢慢变得健步如飞,为了更好地理解和应用小程序优化技巧,在开始实践之前,我们十分有必要看一下小程序整体的运行环境以及启动的流程。小程序的运行环境大体可以分为三类,第一类是iOS端、Mac微信端,第二类是Android端 PC微信端,第三类就是我们开发者经常使用的微信开发者工具模拟器端。另外还有儿童手表上面也有微信,但是那个环境它没有小程序,所以不在我们的讨论范围之内。 三类的运行环境,虽然它们在底层是基于不同的技术实现的,但是它们的启动流程大体上是相似的,小程序的优化主要是指从小程序开始启动到首页完全渲染显示,也就是Page.onReady事件派发,这个过程之间的一个优化。这个过程主要包含了三个流程的节点,这一步包括小程序运行进程及运行环境的准备,这里面具体又包括拉取小程序基本信息,包括代码包版本 地址等信息,另外还有Native小程序进程和微信基本模块的一个初始化。例如在Android环境里面有Activity活动组件的一个初始化,再往下是代码包的下载 校验以及初始化,再往后是系统组件 WebView组件容器和原生组件的一个初始化,最后是JS引擎初始化以及域的创建。 下面我们看第二步关于代码注入,这一步主要包含两大部分:第一部分是框架及第三方基础代码的一个初始化,这里面又分为三个小部分: 第一部分是小程序基础库的注入; 第二小部分是扩展库,例如我们在配置文件里面通过使用useExtendedLib引入的WeUI以及kbone这样的一个类库的初始化; 第三小部分是插件、自定义组件 扩展库代码的一个注入。 第二部分是开发者代码的一个注入,这个里面主要分为两个小部分: 第一个小部分是开发者逻辑层代码的一个注入,这里会派发小程序里面的App.onLaunch还有App.onShow这些事件的一个派发,这些事件都是在这个阶段进行派发的; 第二小部分是开发者视图层代码的一个注入,包括公共代码以及页面代码的一个注入。 下面我们看第三部分关于首屏渲染,这个部分大致可以分为五个小部分: 第一小部分是页面的初始化,这个时间点是initDataSendTime这个时间点的一个触发时机,会有Page.onLoad一个事件的派发; 第二小部分是时间点走到viewLayerReaderStartTime这样的一个时间点的阶段,这个时候会有Page.onShow事件的一个派发; 第三小部分是开发者代码从后端拉取数据,准备data数据,这个时候也是一个阶段,是第三小部分; 第四小部分是页面的一个整体的渲染; 第五部分是当这个时间点走到viewLayerReaderEndTime这个时间点的时候,它会有一个Page.onReady事件的派发,这个时候就标志着我们首屏渲染的一个完成。 小程序采用逻辑层、视图层双线程运行机制,Native的工作准备它是先于这两个线程开始之前开始的,基础的执行环境准备好以后,逻辑层与视图层两个线程才开始工作,并且两个线程几乎是并发执行的。在视图层与逻辑层它代码完全注入以后,这个时间点它会对齐以后才会进入下一个阶段,也就是首屏渲染这个阶段的开始执行。 这些节点它并不是每一次小程序启动时都会经历的,有些会有,有些不会有。微信有运行环境预加载机制,如果小程序在启动时命中了预加载的环境,有关准备运行环境的节点就可以省略掉,这一部分的启动时间也可以节省了。对于开发者紧跟小程序框架的更新,及时使用用户覆盖率最广的基础库版本,让自己的小程序运行环境大众化、普通化,则有助于预加载环境的一个命中,终端类型不同经历的节点也不尽相同。 在Android上小程序启动的时候,微信它开启了新线程,在iOS上则没有 iOS上小程序它在启动的时候,它会复用与微信相同的一个进程,因此Android上有小程序进程与Activity初始化这样的一个节点,在iOS上则没有,再加上iOS的设备它普遍的性能是高于Android设备的,所以这使得iOS的设备的启动的效率普遍就高于Android。 对于相同版本的小程序在同一性能级别的设备上运行,iOS设备平均会比Android大概会少0.5的这样一个启动时间,另外还有启动方式对流程经历的节点也有影响。 下面我们看一下启动方式。小程序按启动方式不同分为冷启动和热启动两种方式。 什么是冷启动?什么是热启动?如果小程序在用户设备上是第一次打开或者是销毁后再次打开,这个时候的启动就是冷启动。热启动是相对冷启动而言的,热启动是小程序启动的一种优化机制,小程序进入后台30分钟以内再次进入前台,可以直接从后台状态然后恢复到前台,在这种热启动方式里面,像前面我们提到的小程序基本信息、拉取代码包的下载还有JS引擎初始化等等这些流程节点,甚至像App.onLaunch、Page.onLoad以及Page.onReady这些一次性的流程事件都不会有了。小程序第一次启动以及冷启动30分钟以后被系统回收重新再启动都是冷启动。 前面我们讲的启动流程的主要节点是冷启动流程的节点。我们说的小程序性能优化主要是指冷启动性能的一个优化以及运行时渲染性能的一个优化,在小程序冷启动流程里边涉及到一些程序以及页面事件。下面我们统一看一下这些事件。 在小程序中App与Page都有它们各自的一个生命周期函数,这些周期函数有一些与启动流程是密切相关的。我们先看一下App周期函数,这里面有三个事件需要我们注意: 第一个是onLaunch,它是监听小程序初始化的一个事件; 第二个是onShow 监听小程序启动或切前台这样一个事件; 第三个是onHide 监听小程序切后台这样的一个事件。 下面我们再看一下Page周期函数。这个里面有五个事件是与优化相关的: 第一个是onLoad它监听页面加载; 第二个是onShow监听页面显示; 第三个是onReady监听页面初次渲染完成; 第四个是onHide监听页面隐藏; 第五是onUnload监听页面卸载。 App.onShow事件和Page.onShow事件是视图界面开始显示时派发的,它们会重复派发与启动流程优化密切相关的一次性事件,主要有App.onLaunch Page.onLoad和Page.onReady这三个事件,在这三个事件节点恰当的安排执行合适的逻辑代码是优化的重要技巧一。 至于像App.onShow App.onHide以及Page.onHide Page.onUnload是与运行时性能优化十分相关的一些事件,下面我们根据小程序的冷启动流程以及与其相关的密切相关的一些八个生命周期函数大致讲一下有哪些节点是可以优化的。 第一条在这个环境准备阶段中,在拉取小程序基本信息阶段,这个阶段是有可能优化的。微信对用户设备上经常使用的小程序它会有轮询机制,在轮询的时候会自动拉取小程序的一个基本信息,正常情况下这个小程序的基本信息的一个拉取它是同步的,它会阻塞我们后续流程节点的一个执行,如果通过轮询节省了这样的一个过程,启动流程跳过这个节点时间是可以节约的,当然这个节点开发者基本上做不了什么事情,开发者并不能左右微信的轮询机制。但是越受用户欢迎的一个小程序因为它属于经常使用的小程序,它会命中轮询机制,启动的一个性能也会更好,而那些不被用户经常访问的小程序反而没有这个福利,这大概就是技术里面的一个马太效应,就是好的会更好然后坏的会越差。 针对环境准备阶段微信提供了环境预加载机制,微信客户端会根据用户设备的使用场景和设备资源的一个消耗情况,依据一定的一个策略,在小程序启动之前对运行环境进行部分的预加载,这个过程开发者基本也无法干涉,开发者能做的仅是紧跟小程序基础库的一个更新,积极使用最新的、最普遍的、最广泛的一个基础库版本以及提高预加载环境的一个命中率。 在代码注入阶段,逻辑层与视图层代码都需要注入,两个线程的代码都注入完成以后首屏渲染流程才能开始,Page.onLoad事件才能触发。我们可以想方设法减少代码的一个注入量和复杂度以期减少启动时间,小程序在这方面有分包、有独立分包、有按需注入、有用时加载和占位组件等等这些特性,这些都是这一阶段的一些优化技巧。这些技巧稍后我们在课程里面都会详细介绍。 在合适的生命周期函数节点执行合适的代码也可以优化启动性,Page.onReady事件派发于首屏渲染完成的时候,如果我们要从后端拉取数据并在首页上进行渲染,在这个事件函数里面执行拉取操作,势必会造成二次渲染的CPU资源浪费,但如果我们在Page.onLaunch这个事件触发的时候就开始数据拉取,又可能会阻塞小程序正常的一个启动流程,在这种情况下我们要怎么去做?我们可以使用异步转同步的编程范式以及使用并发复合命令,在多个文件里边对齐这个代码的执行点,这样的话就显得尤为重要了。具体的优化办法,稍后我们在课程里面会详细讲解。 从Page.onLoad事件派发页面开始渲染到Page.onReady这个事件派发首屏渲染完成,这中间涉及到的动态数据加载,其加载的数据量有多少、网络请求所需的时间有多少还有图片等静态资源它加载所需要的时间有多少,都会影响首屏渲染的一个效率,这个阶段使用骨架屏技巧包括压缩图片、提高服务器接口响应效率和数据传输效率等等,这些都可以优化首屏渲染的一个用户体验。针对小程序里面用到的一些数据,微信还提供了数据预加载周期性更新机制,不需要开发者自己去拉取微信就可以代为拉取,小程序在启动的时候,直接取用这些已经加载好的数据就可以了,这也是优化启动流程的一个技巧之一。 当然了这个技巧是微信团队特意为开发者而设计的,针对低端机首次渲染需要较长的一个时间,微信提供了初始渲染缓存机制,启用初始渲染缓存可以使视图层不需要等待逻辑层代码初始化完毕就可以直接提前将这个页面初始化的数据渲染的结果展示给用户。 以上就是针对启动流程中部分节点的一个性能优化技巧,稍后我们在课程里面都会详细地进行讲解在运行的时候针对小程序的双线程运行机制和视图重渲染机制也有相关的一些性能优化技巧。下面我们就再看一下这方面的一些技巧。
2022-07-29 - 小程序构建骨架屏的探索
首屏 一般情况下,在首屏数据未拿到之前,为了提升用户的体验,会在页面上展示一个loading的图层,类似下面这个 [图片] 其中除了菊花图以外网上还流传这各种各样的loading动画,在PC端上几乎要统一江湖了,不过最近在移动端上面看到不同于菊花图的加载方式,就是这篇文章需要分享的Skeleton Screen,中文称之为"骨架屏" 概念 A skeleton screen is essentially a blank version of a page into which information is gradually loaded. 在H5中,骨架屏其实已经不是什么新奇的概念了,网上也有各种方案生成对应的骨架屏,包括我们经常使用的知乎、饿了么、美团等APP都有应用骨架屏这个概念 图片来源网络,侵删 [图片] 方案 先从H5生成骨架屏方案开始说起,总的来说H5生成骨架屏的方案有2种 完全靠手写HTML和CSS方式给每个页面定制一套骨架屏 利用预渲染的方式生成静态骨架屏 第一套方案,毫无疑问是最简单最直白的方式,缺点也很明显,假如页面布局有修改的话,那么除了修改业务代码之外还需要额外修改骨架屏,增加了维护的成本。 第二套方案,一定程度上改善了第一套方案带来的维护成本增加的缺点,主要还是使用工具预渲染页面,获取到DOM节点和样式,保留页面结构,覆盖样式,生成灰色块盖在原有文本、图片或者是canvas等节点上面,最后将生成的HTML和CSS打包出来,就是一个带有骨架屏的页面。最后再利用webpack工具将生成的骨架屏插入到HTML里面,详细的话可以看看饿了么的分享,这里就不多描述了。 调研了下H5生成骨架屏的方案,对于小程序生成骨架屏的方案也有了一个大致的想法,主要有2个难点需要实现 预渲染 获取节点 预渲染 再说回饿了么提供的骨架屏的方案,使用 puppeteer 渲染页面(或者使用服务端渲染,vue或者react都有提供相应的方案),拿到DOM节点和样式,这里有一点需要注意的是,页面的渲染是需要初始化的数据,数据的来源可以是初始化的data(vue)或者mock数据,当然小程序是无法直接使用 puppeteer 来做预渲染(有另外的方案可以实现),需要利用小程序初始化的 data + template 渲染之后得到一个初始化结构作为骨架屏的结构 [代码]//index.js Page({ data: { motto: 'Hello World', userInfo: { avatarUrl: 'https://wx.qlogo.cn/mmopen/vi_32/SYiaiba5faeraYBoQCWdsBX4hSjFKiawzhIpnXjejDtjmiaFqMqhIlRBqR7IVdbKE51npeF6X1cXxtDQD2bzehgqMA/132', nickName: 'jay' }, lists: [ 'aslkdnoakjbsnfkajbfk', 'qwrwfhbfdvndgndghndeghsdfh', 'qweqwtefhfhgmjfgjdfghaefdhsdfgdfh', ], showSkeleton: true }, onLoad: function () { const that = this; setTimeout(() => { that.setData({ showSkeleton: false }) }, 3000) } }) //index.wxml <view class="container"> <view class="userinfo"> <block> <image class="userinfo-avatar skeleton-radius" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname skeleton-rect">{{userInfo.nickName}}</text> </block> </view> <view style="margin: 20px 0"> <view wx:for="{{lists}}" class="lists"> <icon type="success" size="20" class="list skeleton-radius"/> <text class="skeleton-rect">{{item}}</text> </view> </view> <view class="usermotto"> <text class="user-motto skeleton-rect">{{motto}}</text> </view> <view style="margin-top: 200px;"> aaaaaaaaaaa </view> </view> [代码] 有了上面的 data + template 之后,就有了一个初始化的页面结构,接下来就需要拿到节点信息 节点 小程序基础库1.4.0之后小程序基础库提供了一组新的API,可用于获取节点信息,具体API戳这里。 跟H5方式一样,根据class或者id获取节点信息,不同的是只能获取到当前的节点信息,无法获取到其父或者子节点信息,所以只能手动给需要渲染骨架屏的节点添加相应的class或者id [代码]<view class="container"> <view class="userinfo"> <block> <image class="userinfo-avatar skeleton-radius" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname skeleton-rect">{{userInfo.nickName}}</text> </block> </view> <view style="margin: 20px 0"> <view wx:for="{{lists}}" class="lists"> <icon type="success" size="20" class="list skeleton-radius"/> <text class="skeleton-rect">{{item}}</text> </view> </view> <view class="usermotto"> <text class="user-motto skeleton-rect">{{motto}}</text> </view> <view style="margin-top: 200px;"> aaaaaaaaaaa </view> </view> [代码] 约定2个特殊的class作为获取节点信息的标记[代码]skeleton-rect[代码]和[代码]skeleton-radius[代码],在页面中获取相应的[代码]top[代码]、[代码]left[代码]、[代码]width[代码]、[代码]height[代码]进行骨架屏的绘制 结果 [图片] 具体的调用方式和源码,请看 github ,最后求start 总结 上文有说到小程序也可以使用 page-skeleton-webpack-plugin 方式一样生成骨架屏,最重要的一点就是需要将小程序跑在chrome上面,后面的流程就一样了,至于怎么将小程序跑在chrome上面呢?可以利用 wept ,缺点就是目前作者已经停止维护这个工具了,不支持新版小程序的API。 说回来我这个生成骨架屏的方案,其实跟 page-skeleton-webpack-plugin 有点相似,不同的是,page-skeleton-webpack-plugin 采用离线渲染的方式生成静态骨架屏插入路由中,而我采用运行时先渲染页面默认结构,然后根据默认结构再绘制骨架屏。从性能角度出发确实不如 page-skeleton-webpack-plugin,但是也差不了多少了,主要还是小程序并没有提供类似服务端渲染的方案。目前从使用上来讲,还是有点小麻烦,需要默认数据撑开页面结构,需要给相应的节点添加class,后面有时间再研究下有没有更好的方案吧~~~
2019-02-20 - 自定义导航栏所有机型的适配方案
写在前面的话 大家看到这个文章时一定会感觉这是在炒剩饭,社区中已经有那么多分享自定义导航适配的文章了,为什么我还要再写一个呢? 主要原因就是,社区中大部分的适配方案中给出的大小是不精确的,并不能完美适配各种场景。 社区中大部分文章给到的值是 iOS -> 44px , Android -> 48px 思路 正常来讲,iOS和Android下的胶囊按钮的位置以及大小都是相同且不变的,我们可以通过胶囊按钮的位置和大小再配合 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 来计算出导航栏的位置和大小。 小程序提供了一个获取菜单按钮(右上角胶囊按钮)的布局位置信息的API,可以通过这个API获取到胶囊按钮的位置信息,但是经过实际测试,这个接口目前存在BUG,得到的值经常是错误的(通过特殊手段可以偶尔拿到正确的值),这个接口目前是无法使用的,等待官方修复吧。 下面是我经过实际测试得到的准确数据: 真机和开发者工具模拟器上的胶囊按钮不一样 [代码]# iOS top 4px right 7px width 87px height 32px # Android top 8px right 10px width 95px height 32px # 开发者工具模拟器(iOS) top 6px right 10px width 87px height 32px # 开发者工具模拟器(Android) top 8px right 10px width 87px height 32px [代码] [代码]top[代码] 的值是从 [代码]statusBarHeight[代码] 作为原点开始计算的。 使用上面数据中胶囊按钮的高度加 [代码]top[代码] * 2 上再加上 [代码]statusBarHeight[代码] 的高度就可以得到整个导航栏的高度了。 为什么 [代码]top[代码] * 2 ?因为胶囊按钮是垂直居中在 title 那一栏中的,上下都要有边距。 扩展 通过胶囊按钮的 [代码]right[代码] 可以准确的算出自定义导航的 [代码]左边距[代码]。 通过胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]右边距[代码] 。 通过 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]windowWidth[代码] - 胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]width[代码] 。 再扩展 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 每个机型都不一样,刘海屏得到的数据也是准确的。 如果是自定义整个页面,iPhone X系列的刘海屏,底部要留 [代码]68px[代码] ,不要问我为什么! 代码片段 https://developers.weixin.qq.com/s/Q79g6kmo7w5J
2019-02-25