- 企鹅辅导课程详情页毫秒开的秘密 - PWA 直出
[图片] 天下武功,唯 (wei) 快(fu) 不(bu) 破(po)。 随着近几年的前端技术的高速发展,越来越多的团队使用 React、Vue 等 SPA 框架作为其主要的技术栈。以 React 应用为例,从性能角度,其最重要的指标可能就是首屏渲染所花费的时间了。那么今天,我们要给大家分享的一个把优化做到极致的故事。 我们的目标是让 H5 的页面也能够拥有 Native 般的体验,如果你还在寻求什么技术能够让老板虎躯一震(拯救你的KPI),那么这篇文章或许能够帮助到你。 企鹅辅导课程详情页是什么 [图片] 课程详情页是腾讯旗下 K12 在线教育产品 企鹅辅导 APP 中最重要页面之一,也是流量最大的页面之一,所以它的打开速度也是至关重要的。 这是一个使用 [代码]React[代码] 编写的 H5 页面,运行于多端,包括: [代码]企鹅辅导APP[代码]、[代码]手机 QQ[代码]、[代码]手机浏览器[代码]。 架构演变 纯异步渲染 我们知道当前主流的 SPA 的应用的默认渲染方式都是这样的: [图片] 在这种情况下,从加载页面到用户看到页面(首屏渲染所花费的时间)就是上图中灰色边框区域所包括的时间。 这是最慢的一种方式,就算 CGI 够快,最少要花费 1S 到 2S 左右的时间了。 接着我们简单优化一下: 把静态资源缓存起来,这样下次用户打开的时候就不用从网络请求了。 第 ④ 步拉取 CGI 这个动作是否可以提前呢?我们可以在请求 HTML 之后,先通过一段 JS 脚本去请求 CGI 数据,后面第 **④ **步的时候,就可以直接拿到数据了,这就是 CGI 预加载。 怎么做到呢?我们的方案是统一封装 Request 请求工具,在用 Webpack 打包的时候,会往页面顶部注入一段 预加载 CGI 的 JS 代码,维护一个CGI 与 DATA 对应 MAP,后面发请求的时候,先去 MAP 里取值,如果有值的话直接拿出来,没有的话则发起HTTP 请求。(具体请查阅我们团队开源的 Preload 工具) [图片] 这种模式还有一些其他的优化的方法: 在 HTML 内实现 Loading 态或者骨架屏; 去掉外联 css; 使用动态 polyfill; 使用 SplitChunksPlugin 拆分公共代码; 正确地使用 Webpack 4.0 的 Tree Shaking; 使用动态 import,切分页面代码,减小首屏 JS 体积; 编译到 ES2015+,提高代码运行效率,减小体积; 使用 lazyload 和 placeholder 提升加载体验。 这种模式的优化不是我们这次讲述的重点,想了解的童鞋可以查看这篇文章。 效果如下图所示: [图片] 直出同构 在异步的模式下,除了上述优化,我们还在端内(企鹅辅导 APP、手机 QQ)内做了离线包缓存(腾讯手Q方面独立研发出来的针对手机端优化的方案,简而言之就是将静态资源缓存在手机 APP 内),经过我们的数据测试,首屏渲染大概能够达到秒开(1s左右) 的效果。 [图片] 但对有着性能极致追求的我们来说,肯定是不会满意的。 继续优化,最容易、最大众的套路肯定就是直出(服务端渲染)了。 [图片] 现在直出的方案已经有很多很多种,这里也不多做介绍了,如果您想了解更多关于服务端渲染的方案,请参考这篇文章。 直出针对首屏时间的优化效果是非常明显的,经过我们的测试,数据大概能够提升**25%**左右。 直出之后的效果如下图: [图片] 可以看到对于首屏来说,没有了**【加载中…】**的等待时间,视觉体验提升了不少。 PWA 直出 [图片] 针对上述、常见的直出应用来说,我们能够优化的点在哪里呢?让我们来详细分析一波,这也是今天我们要给大家分享的重点。 首先看看直出应用各个环节的耗时表 (本地环境 2018款 iMac): 过程名称 过程花费 Node 内 CGI 拉取 300 ms RenderToString 20 ms 网络耗时 10 ms 前端HTML渲染 30 ms 从上面的表中我们看出,直出渲染的耗时的大头还是在 CGI 接口的拉取上。 我们现在提出两个问题: CGI 接口的数据是否可以缓存 ? HTML 又是否可以缓存 ? 一、接口的动静分离 [图片] 这个页面的接口数据中,有一些数据,是实时变动的, 比如:当前还剩多少个名额、此时此刻课程的价格、用户是否购买过这个课程等。 这些数据的特性决定了这个数据接口不能够被缓存。(假设将其缓存,那么就会存在可能用户进来看到当前还剩下10个名额,其实课程已经卖光了的情况) 为了这个时间耗时的大头,我们做了CGI接口的动静分离。 将与用户态、当前时间没有关联的数据(比如[代码]课程标题[代码]、[代码]课程上课的时间[代码]、[代码]试听模块的地址[代码]等)放在一个接口(静态接口),其他变化的数据放在另一个接口(动态接口)。 那么可以使用静态的接口来做服务端渲染,好处是第一比较快(少了动态的信息,而且后台也可以做缓存),第二 Node 直出可以做缓存了。 二、直出 Redis 缓存 这样我们就可以将那部分静态的、不会经常变动的数据用来直出 HTML,然后将这个 HTML 文件缓存到 Redis 中。 客户端请求此网页,Node 端接受到请求之后,先去 Redis 里拿缓存的 HTML,如果 Redis 缓存没有命中,则拉取静态的 CGI 接口渲染出 HTML存入 Redis。 客户端拿到 HTML 之后,会立刻渲染,然后再用 JS 去请求动态的数据,渲染到相应的地方。 [图片] 做完之后我们可以看到优化效果的提升是非常非常明显的: [图片] 直接从 262ms 提升到了 16ms !(本地环境),简直飞一般的感觉,妈妈再也不用担心领导看耗时了。 三、PWA 直出缓存 关于什么是 PWA ,以及如何使用,请移步这篇文章。 做了 Node 端直出的 HTML 缓存之后,我们接着优化,接着思考,是否可以在客户端也缓存 HTML,这样连网络延时这部分消耗也省掉呢。 答案就是使用 PWA 在客户端做离线缓存,将我们直出的 HTML 缓存在客户端,每次用户请求的时候,直接从 PWA 离线缓存里取出对应的直出页面(HTML)响应给用户,响应之后紧接着请求 Node 服务更新本地的 PWA 缓存。(如下图所示) [图片] 核心代码: [代码]self.addEventListener("fetch", event => { // TODO other logic (maybe fetch filter) // core logic event.respondWith( caches.open(cacheName).then(function(cache) { return cache.match(cacheCourseUrl).then(function(response) { var fetchPromise = fetch(cacheCourseUrl).then(function( networkResponse ) { if (networkResponse.status === 200) { cache.put(cacheCourseUrl, networkResponse.clone()); } return networkResponse; }); return response || fetchPromise; }); }) ); }); [代码] 废话不多说,先看效果对比 (左 PWA 直出;右 离线包): [图片] 从上图可以看出,使用了 PWA 直出缓存之后,首屏渲染基本是毫秒开,可以说与 Native 并肩了。 经过我们的数据测试,使用 PWA 直出缓存,首屏渲染的时间最好可以到400ms左右级别: [图片] PWA 直出细节优化 一、防页面跳动 因为对接口进行了动静分离,使用静态接口直出页面,然后在客户端拉取动态数据渲染完。这就可能会导致页面的抖动(比如详情页中的试听模块,是在客户端渲染的)。 [图片] 因为高度改变了,视觉上就会出现抖动(具体可以参考上面章节直出时候的 GIF 截图)。 要去掉页面抖动的情况,就必须保证容器的高度在直出时候已经存在了。 比如这个试听模块,其实这个封面图和试听按钮是可以在服务端渲染出来的,而后面的 Video 模块则必须要在客户度渲染(腾讯云 Tcplayer)。 所以这里可以拆分成:(试听封面 + 按钮 + 时间)服务端渲染 + 底层 Video(客户端渲染)。 有些需要在客户端计算高度的容器(表现为常放在 ComponentDidMount 里计算),如果它们依赖客户端环境(比如依赖当前系统是安卓还是 IOS),就导致他们肯定不能放在服务端直接渲染出来,这又怎么办呢? 这里我们的做法,是将这些计算放在 HTML body 之前,通过内联的脚本嵌入,计算出当前环境,给 body 加上一个特定的类(class),然后在这个特定的类下面的元素,就可以通过 css 给予特定的样式。比如下面代码: [代码]/* * 因为在不同的手机 APP 环境内,页面的 padding 是不一样的。 * 我们要在页面渲染完之前加上相应的 padding */ var REGEXP_FUDAO_APP = /EducationApp/; if ( typeof navigator !== "undefined" && REGEXP_FUDAO_APP.test(navigator.userAgent) ) { if (/Android/i.test(navigator.userAgent)) { document.body.classList.add("androidFudaoApp"); } else if (/iPhone|iPad|iPod|iOS/i.test(navigator.userAgent)) { if (window.screen.width === 375 && window.screen.height === 812) { document.body.classList.add("iphoneXFudaoApp"); } else { document.body.classList.add("iosFudaoApp"); } } } [代码] [代码].androidFudaoApp .tt { padding-top: 48px; background-position-y: 84px; } .iphoneXFudaoApp .tt { padding-top: 88px; background-position-y: 124px; } .iosFudaoApp .tt { padding-top: 64px; background-position-y: 100px; } [代码] 然后把这段代码通过构建插入到页面 body 之前。 [图片] 防抖动优化效果如下 (左优化完,右未优化): [图片] 二、冷启动预加载 虽然我们做了 PWA 离线缓存,但是对于冷启动来说,客户端里面的 PWA 缓存还是没有的,这样就会导致初次点击页面,渲染速度相对慢一点。 这里我们可以在 APP 启动的时候,用一个预加载的脚本最大限度的拉取用户可能访问的页面。 核心代码如下: [代码]// 预加载页面时, PWA 预缓存课程详情页面的直出 function prefetchCache(fetchUrl) { fetch("https://you preFetch Cgi") .then(data => { return data.json(); }) .then(res => { const { courseInfo = [] } = res.result || {}; courseInfo.forEach(item => { if (item.cid) { caches.open(cacheName).then(function(cache) { fetch(`${courseURL}?course_id=${item.cid}`).then(function( networkResponse ) { if (networkResponse.status === 200) { cache.put( `${courseURL}?course_id=${item.cid}`, networkResponse.clone() ); } // return networkResponse; }); }); } }); }) .catch(err => { // To monitor err }); } [代码] PWA 直出遗留问题 一、兼容性问题 随着 PWA 技术的发展,现今大部分手机以及 PC 环境已经支持对 PWA 进行了支持。经过我们的测试发现:安卓基本上都是支持的,IOS 需要11.3以上才支持。 Service Workers 兼容性图 [图片] 具体的兼容性支持点我查看。 二、IOS 渲染问题 很多的经验告诉我们,外联的 script 标签要放在 body 的后面,因为它会阻塞页面的 DOM 渲染。 经过测试发现,IOS 的 [代码]WebView[代码] ([代码]UIWebView[代码])渲染机制并不会上述一样,而是要等到后面的 JS 执行完之后才渲染页面,如果是这样,我们的直出渲染优化就没有效果了(因为 HTML 并不在最开始渲染),这里可以使用 [代码]script[代码] 标签的 [代码]async[代码] 与 [代码]defer[代码] 属性来达到异步渲染的作用。 升级 WkWebView 之后,情况得到改善,渲染正常。 附录 参考资料 PWA 的探索与最佳实践 亿万级访问量下的前端同构直出实践 React 16 加载性能优化指南 更多基于 PWA 的性能优化实践,请查看 IMWeb 团队刘华的分享。.com/course/16777)。
2019-03-15 - 如何提升你的云函数性能
在使用云开发一段时间后,你一定会遇见一个问题:虽然云函数非常的方便,但我的云函数似乎性能不够好,为什么我的云函数每次加载都需 2 ~ 3 秒种,时间太长了!。 这篇文章,就来告诉你,应该如何提升你的云函数性能。 如何了解云函数运行情况? 在了解如何优化云函数的运行情况之前, 我们需要先了解,如何查看当前的云函数运行情况,这样才能有个对比。 [图片] 打开小程序开发者工具,并打开你的项目 进入到你要调试的页面,打开调试器 调用云函数,并在调试器中切换到 Network 页面,找到你的请求。 点击你的请求,然后切换到 Timing 页面,查看具体的情况。 在这个页面中,你可以理解其中的 Waiting(TTFB) 是你发起请求到你接收到返回结果的第一个字节的时间,简单的来说,就是服务器计算结果需要花费的时间。而下方的 Content Download 则是下载内容所需的时间,你可以理解是表现出网络速度快慢的数据。 总结来说,就是如果 Waiting TTFB 的值比较大,你就去优化云函数性能。如果 Content Download 的数值毕竟大,你就需要优化网络情况 优化 Waiting TTFB 云函数的运行机制 Waiting TTFB 的优化是云函数性能端的优化,那么在优化之前,我们就需要先来了解一下云函数的运行机制,以便帮助我们了解应该如何去进行性能优化。 [图片] 在蕴含运行时,具体的顺序是这样的 用户发起请求,请求发送到云开发的后台 云开发后台的调度器将请求分发给下方的执行的 worker 、容器 容器创建环境、下载代码 执行代码 在这个过程中,发起请求到云开发、调度器调度速度、调度器传递信息到容器、函数调用等,都是可以优化的,但是我们在具体的使用过程中。这些大都需要由云开发的工作人员来完成,对于我们自己来说,只能去尽可能的优化容器内部到代码层面的东西。 接下来,我们可以看看更细致的调用逻辑。 [图片] 在云开发中,我们可以将调用分为三种类型: 冷启动:图中的红色阶段,需要重新创建容器、下载代码,耗时最长 温启动:图中的黄色阶段,需要下载代码,耗时较长 热启动:图中的蓝色阶段,不需要下载代码,耗时最短 我们可以看到,最快的,是热启动,函数不需要创建容器,不需要启动函数就可以完成执行,显然比要创建容器或要下载代码的温启动和冷启动速度更快。这样,我们就得到了优化云函数性能的第一个方法 1. 让你的云函数每次调用都走热启动 当我们可以让我们的云函数的每一次调用都走热启动,少了容器的创建和函数的部署,请求的速度理所当然的要比冷启动和温启动更快。 我们可以测试一下,我设置每秒调用一次云函数,看看 TTFB 的变化。 [代码]setInterval(()=>{wx.cloud.callFunction({name:'profile'})},1000) [代码] 函数内代码是默认创建的云函数代码。 则对应的执行效果如下 [图片] 可以看到,函数的执行时间从第一次的 1.2s 降低到了 200ms左右,性能提升了 80%,我们仅仅是简单的提升了函数的调用频次,就可以实现提升函数的调用性能,这就是热启动带给我们的价值。 实施方案 如果你需要足够高的性能,不妨借助云开发的定时器,定期唤起你的容器,从而为你的容器保活,确保你的函数时刻被热启动。 2. 缩小你的函数大小 在前面我们曾介绍过,云函数在启动过程中,会创建容器和下载代码。创建容器的过程对于开发者来说不可控,不过我们可以使用一些方法,缩小我们的代码,提升代码的下载速度,比如说,缩小我们的函数代码。 这里我们可以做个测试,这里我创建了两个函数,两个函数的代码完全一致,不同的是,在实验组的函数中,我加入了一个 temp 变量的声明,这个变量的值是一个非常长的字符串,从而使得两个函数的大小分别是 68K 和 4K。 接下来,我们看看二者的执行时间。 [图片] 我们会发现,几乎没有差距的代码,因为加入了变量声明的因素,在性能上会略慢几毫秒,后续随着容器的不断复用,函数的之间的差距也越来越小,几乎可以忽视。 实施方案 对于你的代码,要尽可能的精炼,减少无用的代码,减少代码下载所需时间。 3. 削减不需要的 Package 除了下载代码以外,还需要下载 Node 环境运行所需的依赖包,虽然云开发可能针对 Node Modules 已经做了缓存,但依然存在下载的时间差区别,这里我也做了一个实验。 空包:什么都没装,把 wx-server-sdk 都卸载掉了。 复杂包:装了 Mongoose、sequelize、sails 等依赖的包。 函数逻辑上也相差无几,都是返回 Event ,则结果如下 [图片] 我们发现,前三次可能是因为涉及到依赖包的下载问题,所以前三次的时长大小对比特别的明显,而从第四次开始,二者的区别就不大了,可能是因为依赖已经完成了缓存,所以可以直接使用缓存来完成函数的执行。 实施方案 你可以选择看看你的 package.json ,看看其中是否有你不需要的依赖,将其删除,仅保留有需要的依赖,可以有效提升你的代码执行速度。 优化 Content Download 如果你想要优化 Content Download ,核心需要优化的是两个点: 手机到服务端的节点的距离和速度 内容的大小 前者一般来说,你可以通过切换不同的网络环境来实现优化,比如从 3G 切换到 4G ,从 4G 升级到 5G,这些都可以提升你的手机到服务端节点之间的速度。 此外,还可以借助内容分发网络 CDN 能力来完成缩小你到服务端节点之间的距离,不过对于云函数来说,因为你不可控,无法控制,所以这一点不再谈。 这里补充一句,云开发的文件存储都是有 CDN 的,因此,你通过云存储下载的文件才会比别人更快。 后者则一般通过调整代码来完成,比如只返回必须的资源,对于不需要的内容,不再返回,或压缩返回。 总结 最后,我们回顾一下这篇文章中介绍的优化云函数的方法: 函数下载性能优化 保持函数容器的热启动,提升函数启动性能 缩小函数大小,提升代码下载速度 削减不必要的包,减少依赖大小 网络优化 使用更好的网络,比如 Wi-Fi 云函数中仅返回所需要的内容,减少下载时间。 以上这些方法,你都在你的函数中试过么?有没有其他的优化方法?欢迎你与我分享。
2019-12-08 - 个体工商户注册的小程序 如何绑定银行卡 ?
就是个体工商户注册小程序如何绑定银行卡 里面钱拿不到😅😅😅
2020-08-10 - 在图片数量和大小未知的情况下,采取缓存的策略科学吗?
我在用小程序提供的云开发能力开发一个实验类小程序,一个实验包含若干张图片,每个实验步骤会显示一段文字和一张图片,图片保存在云存储中,每进入一个页面就访问一次数据库得到图片Url进而将该图片显示在界面。 实际操作发现进入页面后文字和图片不能同时显示在界面上,文字秒刷新出来,但是图片总是“顿一下”才显示,虽然用时很短只感觉“顿一下”,但是实验精度要求到【毫秒级】,这种“顿一下”势必会影响实验精度和实验效果。 我想在实验进行之前将所有图片提取缓存下来,这样进入实验页面后图片加载是不是就和文字一样秒刷新出来了,我并不确定一个实验究竟包含多少图片,也不确定实验发起者上传的图片大小,请问通过缓存手段达到实验各个页面显示时图片和文字一样无卡顿感可行吗? 谢谢! 最后:实验过程中要求不能有卡顿,实验要求精确到毫秒,肉眼可见的卡顿已经影响到实验效果了。请问这类实验平台是不是不适合用小程序来开发?
2020-05-18