让老板虎躯一震的前端技术,KPI杀手

qcloudcommunity · · 1389 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

> 本文由云+社区发表 ![img](https://ask.qcloudimg.com/http-save/2510799/0nw4seg881.jpeg?imageView2/2/w/1620) 天下武功,唯 (wei) 快(fu) 不(bu) 破(po)。 随着近几年的前端技术的高速发展,越来越多的团队使用 React、Vue 等 SPA 框架作为其主要的技术栈。以 React 应用为例,从性能角度,其最重要的指标可能就是**首屏渲染**所花费的时间了。那么今天,我们要给大家分享的一个把优化做到极致的故事。 我们的目标是让 **H5 的页面也能够拥有 Native 般的体验**,如果你还在寻求什么技术能够让老板**虎躯一震**(拯救你的KPI),那么这篇文章或许能够帮助到你。 #### 企鹅辅导课程详情页是什么 ![img](https://ask.qcloudimg.com/draft/2510799/zo3medcd9j.png?imageView2/2/w/1620)企鹅辅导详情页 课程详情页是腾讯旗下企鹅辅导 APP 中最重要页面之一,也是流量最大的页面之一,所以它的打开速度也是至关重要的。 这是一个使用 `React` 编写的 H5 页面,运行于多端,包括: `企鹅辅导APP`、`手机 QQ`、`手机浏览器`。 #### 架构演变 ##### 纯异步渲染 我们知道当前主流的 SPA 的应用的默认渲染方式都是这样的: ![img](https://ask.qcloudimg.com/http-save/2510799/guk7pjrjb2.jpeg?imageView2/2/w/1620) 在这种情况下,从加载页面到用户看到页面(**首屏渲染**所花费的时间)就是上图中灰色边框区域所包括的时间。 这是最慢的一种方式,就算 CGI 够快,最少要花费 **1S** 到 **2S** 左右的时间了。 接着我们**简单优化**一下: - 把静态资源缓存起来,这样下次用户打开的时候就不用从网络请求了。 - 第 **④** 步拉取 CGI 这个动作是否可以提前呢?我们可以在请求 HTML 之后,先通过一段 JS 脚本去请求 CGI 数据,后面第 **④** 步的时候,就可以直接拿到数据了,这就是 **CGI 预加载**。 怎么做到呢?我们的方案是统一封装 Request 请求工具,在用 Webpack 打包的时候,会往页面顶部注入一段 预加载 CGI 的 JS 代码,维护一个CGI 与 DATA 对应 MAP,后面发请求的时候,先去 MAP 里取值,如果有值的话直接拿出来,没有的话则发起HTTP 请求。(具体请查阅我们团队开源的 [Preload](https://github.com/imweb/preload) 工具) ![img](https://ask.qcloudimg.com/http-save/2510799/ggyfk42cm1.jpeg?imageView2/2/w/1620) 这种模式还有一些其他的优化的方法: - 在 HTML 内实现 Loading 态或者骨架屏; - 去掉外联 css; - 使用动态 polyfill; - 使用 SplitChunksPlugin 拆分公共代码; - 正确地使用 Webpack 4.0 的 Tree Shaking; - 使用动态 import,切分页面代码,减小首屏 JS 体积; - 编译到 ES2015+,提高代码运行效率,减小体积; - 使用 lazyload 和 placeholder 提升加载体验。 效果如下图所示: ![img](https://ask.qcloudimg.com/http-save/2510799/q2sacc5ujd.gif)异步渲染 ##### 直出同构 在异步的模式下,除了上述优化,我们还在端内(企鹅辅导 APP、手机 QQ)内做了**离线包缓存**(腾讯手Q方面独立研发出来的针对手机端优化的方案,简而言之就是将静态资源缓存在手机 APP 内),经过我们的数据测试,首屏渲染大概能够达到秒开(1s左右) 的效果。 ![img](https://ask.qcloudimg.com/http-save/2510799/5cw687p3u5.jpeg?imageView2/2/w/1620)-w300 但对有着性能极致追求的我们来说,肯定是不会满意的。 继续优化,**最容易、最大众**的套路肯定就是直出(**服务端渲染**)了。 ![img](https://ask.qcloudimg.com/http-save/2510799/zsifo5e5bh.jpeg?imageView2/2/w/1620) 现在直出的方案已经有很多很多种,这里也不多做介绍了,如果您想了解更多关于服务端渲染的方案,请参考这篇文章。 直出针对首屏时间的优化效果是非常明显的,经过我们的测试,数据大概能够提升**25%**左右。 直出之后的效果如下图: ![img](https://ask.qcloudimg.com/http-save/2510799/zchtndkia8.gif)直出同构 可以看到对于首屏来说,没有了**【加载中...】**的等待时间,视觉体验提升了不少。 #### PWA 直出 ![img](https://ask.qcloudimg.com/http-save/2510799/yzfpp4da85.jpeg?imageView2/2/w/1620)PWA 针对上述、常见的直出应用来说,我们能够优化的点在哪里呢?让我们来详细分析一波,这也是今天我们要给大家分享的重点。 首先看看直出应用各个环节的耗时表 (**本地环境 2018款 iMac**): | 过程名称 | 过程花费 | | ---------------- | -------- | | Node 内 CGI 拉取 | 300 ms | | RenderToString | 20 ms | | 网络耗时 | 10 ms | | 前端HTML渲染 | 30 ms | 从上面的表中我们看出,直出渲染的耗时的大头还是在 **CGI 接口的拉取**上。 我们现在提出**两个问题**: - CGI 接口的数据是否可以缓存 ? - HTML 又是否可以缓存 ? ##### 一、接口的动静分离 ![img](https://ask.qcloudimg.com/draft/2510799/zo3medcd9j.png?imageView2/2/w/1620)动态信息 这个页面的接口数据中,有一些数据,是实时变动的, 比如:当前还剩多少个名额、此时此刻课程的价格、用户是否购买过这个课程等。 这些数据的特性决定了这个**数据接口不能够被缓存**。(假设将其缓存,那么就会存在可能用户进来看到当前还剩下10个名额,其实课程已经卖光了的情况) 为了这个时间耗时的大头,我们做了**CGI接口的动静分离**。 > 将与用户态、当前时间没有关联的数据(比如`课程标题`、`课程上课的时间`、`试听模块的地址`等)放在一个接口(静态接口),其他变化的数据放在另一个接口(动态接口)。 那么可以使用静态的接口来做服务端渲染,好处是第一比较快(少了动态的信息,而且后台也可以做缓存),第二 Node 直出可以做缓存了。 ##### 二、直出 Redis 缓存 这样我们就可以将那部分静态的、不会经常变动的数据用来直出 HTML,**然后将这个 HTML 文件缓存到 Redis 中**。 客户端请求此网页,Node 端接受到请求之后,先去 Redis 里拿缓存的 HTML,如果 Redis 缓存没有命中,则拉取静态的 CGI 接口渲染出 HTML存入 Redis。 客户端拿到 HTML 之后,会立刻渲染,然后再用 JS 去请求动态的数据,渲染到相应的地方。 ![img](https://ask.qcloudimg.com/http-save/2510799/6auz4k3ab4.jpeg?imageView2/2/w/1620) 做完之后我们可以看到优化效果的提升是非常非常明显的: ![img](https://ask.qcloudimg.com/http-save/2510799/vs4duvs03f.jpeg?imageView2/2/w/1620) 直接从 **262ms** 提升到了 **16ms** !(本地环境),简直飞一般的感觉,妈妈再也不用担心领导看耗时了。 ##### 三、PWA 直出缓存 > 关于什么是 PWA ,以及如何使用,请移步这篇文章。 做了 Node 端直出的 HTML 缓存之后,我们接着优化,接着思考,是否可以在客户端也缓存 HTML,这样连**网络延时这部分消耗也省掉**呢。 答案就是使用 **PWA** 在客户端做离线缓存,将我们直出的 HTML 缓存在客户端,每次用户请求的时候,直接从 **PWA** 离线缓存里取出对应的直出页面(HTML)响应给用户,响应之后紧接着请求 Node 服务更新本地的 **PWA** 缓存。(如下图所示) ![img](https://ask.qcloudimg.com/http-save/2510799/y2a1yhosvb.jpeg?imageView2/2/w/1620) 核心代码: ```js 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 直出;右 离线包): ![img](https://ask.qcloudimg.com/http-save/2510799/db4yfdet4y.gif)duibi 从上图可以看出,使用了 PWA 直出缓存之后,**首屏渲染基本是毫秒开**,可以说与 Native 并肩了。 经过我们的数据测试,使用 PWA 直出缓存,首屏渲染的时间最好可以到**400ms**左右级别: ![img](https://ask.qcloudimg.com/http-save/2510799/0syswu25og.jpeg?imageView2/2/w/1620) #### PWA 直出细节优化 ##### 一、防页面跳动 因为对接口进行了动静分离,使用静态接口直出页面,然后在客户端拉取动态数据渲染完。这就可能会导致页面的抖动(比如详情页中的试听模块,是在客户端渲染的)。 ![img](https://ask.qcloudimg.com/http-save/2510799/yktce5id4h.jpeg?imageView2/2/w/1620) 因为高度改变了,视觉上就会出现抖动(具体可以参考上面章节直出时候的 GIF 截图)。 要去掉页面抖动的情况,就必须保证**容器的高度在直出时候已经存在了**。 比如这个试听模块,其实这个封面图和试听按钮是可以在服务端渲染出来的,而后面的 Video 模块则必须要在客户度渲染(腾讯云 Tcplayer)。 所以这里可以拆分成:(试听封面 + 按钮 + 时间)服务端渲染 + 底层 Video(客户端渲染)。 有些需要在客户端计算高度的容器(表现为常放在 ComponentDidMount 里计算),如果它们依赖客户端环境(比如依赖当前系统是安卓还是 IOS),**就导致他们肯定不能放在服务端直接渲染出来**,这又怎么办呢? 这里我们的做法,是将这些计算放在 HTML body 之前,通过内联的脚本嵌入,计算出当前环境,给 body 加上一个特定的类(class),然后在这个特定的类下面的元素,就可以通过 css 给予特定的样式。比如下面代码: ```js /* * 因为在不同的手机 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 之前。 ![img](https://ask.qcloudimg.com/http-save/2510799/hjwk677uem.jpeg?imageView2/2/w/1620)-w500 防抖动优化效果如下 (**左优化完,右未优化**): ![img](https://ask.qcloudimg.com/http-save/2510799/eftbwmw8kj.gif)duibi_doudong ##### 二、冷启动预加载 虽然我们做了 PWA 离线缓存,但是对于冷启动来说,客户端里面的 PWA 缓存还是没有的,这样就会导致初次点击页面,渲染速度相对慢一点。 这里我们可以在 APP 启动的时候,用一个预加载的脚本最大限度的拉取用户可能访问的页面。 核心代码如下: ```js // 预加载页面时, 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 兼容性图 ![img](https://ask.qcloudimg.com/http-save/2510799/hcava5tsun.jpeg?imageView2/2/w/1620) ##### 二、IOS 渲染问题 很多的经验告诉我们,外联的 script 标签要放在 body 的后面,因为它会阻塞页面的 DOM 渲染。 经过测试发现,IOS 的 `WebView` (`UIWebView`)渲染机制并不会上述一样,而是要等到后面的 JS 执行完之后才渲染页面,如果是这样,我们的直出渲染优化就没有效果了(因为 HTML 并不在最开始渲染),这里可以使用 `script` 标签的 `async` 与 `defer` 属性来达到异步渲染的作用。 升级 WkWebView 之后,情况得到改善,渲染正常。 #### 附录 ##### 参考资料 - [PWA 的探索与最佳实践](https://juejin.im/entry/5ae1861af265da0b886d1ea8) - [亿万级访问量下的前端同构直出实践 ](https://juejin.im/post/59c370d75188256bce40f1fb) - [React 16 加载性能优化指南](https://zhuanlan.zhihu.com/p/37148975) **此文已由作者授权腾讯云+社区在各渠道发布** **获取更多新鲜技术干货,可以关注我们[腾讯云技术社区-云加社区官方号及知乎机构号](https://www.zhihu.com/org/teng-xun-yun-ji-zhu-she-qu/activities)**

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1389 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传