本文由云+社区发表css
做者:思衍Jax前端
天下武功,惟 (wei) 快(fu) 不(bu) 破(po)。android
随着近几年的前端技术的高速发展,愈来愈多的团队使用 React、Vue 等 SPA 框架做为其主要的技术栈。以 React 应用为例,从性能角度,其最重要的指标可能就是首屏渲染所花费的时间了。那么今天,咱们要给你们分享的一个把优化作到极致的故事。ios
咱们的目标是让 H5 的页面也可以拥有 Native 般的体验,若是你还在寻求什么技术可以让老板虎躯一震(拯救你的KPI),那么这篇文章或许可以帮助到你。git
课程详情页是腾讯旗下企鹅辅导 APP 中最重要页面之一,也是流量最大的页面之一,因此它的打开速度也是相当重要的。github
这是一个使用 React
编写的 H5 页面,运行于多端,包括: 企鹅辅导APP
、手机 QQ
、手机浏览器
。web
咱们知道当前主流的 SPA 的应用的默认渲染方式都是这样的:json
在这种状况下,从加载页面到用户看到页面(首屏渲染所花费的时间)就是上图中灰色边框区域所包括的时间。浏览器
这是最慢的一种方式,就算 CGI 够快,最少要花费 1S 到 2S 左右的时间了。缓存
接着咱们简单优化一下:
怎么作到呢?咱们的方案是统一封装 Request 请求工具,在用 Webpack 打包的时候,会往页面顶部注入一段 预加载 CGI 的 JS 代码,维护一个CGI 与 DATA 对应 MAP,后面发请求的时候,先去 MAP 里取值,若是有值的话直接拿出来,没有的话则发起HTTP 请求。(具体请查阅咱们团队开源的 Preload 工具)
这种模式还有一些其余的优化的方法:
效果以下图所示:
在异步的模式下,除了上述优化,咱们还在端内(企鹅辅导 APP、手机 QQ)内作了离线包缓存(腾讯手Q方面独立研发出来的针对手机端优化的方案,简而言之就是将静态资源缓存在手机 APP 内),通过咱们的数据测试,首屏渲染大概可以达到秒开(1s左右) 的效果。
但对有着性能极致追求的咱们来讲,确定是不会满意的。
继续优化,最容易、最大众的套路确定就是直出(服务端渲染)了。
如今直出的方案已经有不少不少种,这里也很少作介绍了,若是您想了解更多关于服务端渲染的方案,请参考这篇文章。
直出针对首屏时间的优化效果是很是明显的,通过咱们的测试,数据大概可以提高**25%**左右。
直出以后的效果以下图:
能够看到对于首屏来讲,没有了**【加载中...】**的等待时间,视觉体验提高了很多。
针对上述、常见的直出应用来讲,咱们可以优化的点在哪里呢?让咱们来详细分析一波,这也是今天咱们要给你们分享的重点。
首先看看直出应用各个环节的耗时表 (本地环境 2018款 iMac):
过程名称 | 过程花费 |
---|---|
Node 内 CGI 拉取 | 300 ms |
RenderToString | 20 ms |
网络耗时 | 10 ms |
前端HTML渲染 | 30 ms |
从上面的表中咱们看出,直出渲染的耗时的大头仍是在 CGI 接口的拉取上。
咱们如今提出两个问题:
这个页面的接口数据中,有一些数据,是实时变更的, 好比:当前还剩多少个名额、此时此刻课程的价格、用户是否购买过这个课程等。
这些数据的特性决定了这个数据接口不可以被缓存。(假设将其缓存,那么就会存在可能用户进来看到当前还剩下10个名额,其实课程已经卖光了的状况)
为了这个时间耗时的大头,咱们作了CGI接口的动静分离。
将与用户态、当前时间没有关联的数据(好比
课程标题
、课程上课的时间
、试听模块的地址
等)放在一个接口(静态接口),其余变化的数据放在另外一个接口(动态接口)。
那么可使用静态的接口来作服务端渲染,好处是第一比较快(少了动态的信息,并且后台也能够作缓存),第二 Node 直出能够作缓存了。
这样咱们就能够将那部分静态的、不会常常变更的数据用来直出 HTML,而后将这个 HTML 文件缓存到 Redis 中。
客户端请求此网页,Node 端接受到请求以后,先去 Redis 里拿缓存的 HTML,若是 Redis 缓存没有命中,则拉取静态的 CGI 接口渲染出 HTML存入 Redis。
客户端拿到 HTML 以后,会马上渲染,而后再用 JS 去请求动态的数据,渲染到相应的地方。
作完以后咱们能够看到优化效果的提高是很是很是明显的:
直接从 262ms 提高到了 16ms !(本地环境),简直飞通常的感受,妈妈不再用担忧领导看耗时了。
关于什么是 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左右级别:
由于对接口进行了动静分离,使用静态接口直出页面,而后在客户端拉取动态数据渲染完。这就可能会致使页面的抖动(好比详情页中的试听模块,是在客户端渲染的)。
由于高度改变了,视觉上就会出现抖动(具体能够参考上面章节直出时候的 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 技术的发展,现今大部分手机以及 PC 环境已经支持对 PWA 进行了支持。通过咱们的测试发现:安卓基本上都是支持的,IOS 须要11.3以上才支持。
Service Workers 兼容性图
不少的经验告诉咱们,外联的 script 标签要放在 body 的后面,由于它会阻塞页面的 DOM 渲染。
通过测试发现,IOS 的 WebView
(UIWebView
)渲染机制并不会上述同样,而是要等到后面的 JS 执行完以后才渲染页面,若是是这样,咱们的直出渲染优化就没有效果了(由于 HTML 并不在最开始渲染),这里可使用 script
标签的 async
与 defer
属性来达到异步渲染的做用。
升级 WkWebView 以后,状况获得改善,渲染正常。
此文已由做者受权腾讯云+社区在各渠道发布
获取更多新鲜技术干货,能够关注咱们腾讯云技术社区-云加社区官方号及知乎机构号