企鹅辅导课程详情页毫秒开的秘密 - PWA 直出

天下武功,惟 (wei) 快(fu) 不(bu) 破(po)。css

-w300

随着近几年的前端技术的高速发展,愈来愈多的团队使用 React、Vue 等 MVVM 框架做为其主要的技术栈。以 React 应用为例,从性能角度,其最重要的指标可能就是首屏渲染所花费的时间了。那么今天,咱们要给你们分享的一个把优化作到极致的故事。前端

咱们的目标是让 H5 的页面也可以拥有 Native 般的体验,若是你还在寻求什么技术可以让老板虎躯一震(拯救你的KPI),那么这篇文章或许可以帮助到你。android

企鹅辅导课程详情页是什么

课程详情页是腾讯旗下 企鹅辅导 APP 中最重要页面之一,也是流量最大的页面之一,因此它的打开速度也是相当重要的。ios

这是一个使用 React 编写的 H5 页面,运行于多端,包括: 企鹅辅导APP手机 QQ手机浏览器git

架构演变

纯异步渲染

咱们知道当前主流的 SPA 的应用的默认渲染方式都是这样的:github

在这种状况下,从加载页面到用户看到页面(首屏渲染所花费的时间)就是上图中灰色边框区域所包括的时间。web

这是最慢的一种方式,就算 CGI 够快,最少要花费 1S2S 左右的时间了。json

接着咱们简单优化一下:浏览器

  • 把静态资源缓存起来,这样下次用户打开的时候就不用从网络请求了。
  • 步拉取 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 提高加载体验。

这种模式的优化不是咱们此次讲述的重点,想了解的童鞋能够查看这篇文章

效果以下图所示:

-w300

直出同构

在异步的模式下,除了上述优化,咱们还在端内(企鹅辅导 APP、手机 QQ)内作了离线包缓存(腾讯手Q方面独立研发出来的针对手机端优化的方案,简而言之就是将静态资源缓存在手机 APP 内),通过咱们的数据测试,首屏渲染大概可以达到秒开(1s左右) 的效果。

-w300

但对有着性能极致追求的咱们来讲,确定是不会满意的。

继续优化,最容易、最大众的套路确定就是直出(服务端渲染)了。

如今直出的方案已经有不少不少种,这里也很少作介绍了,若是您想了解更多关于服务端渲染的方案,请参考这篇文章。

直出针对首屏时间的优化效果是很是明显的,通过咱们的测试,数据大概可以提高**25%**左右。

直出以后的效果以下图:

-w300

能够看到对于首屏来讲,没有了**【加载中...】**的等待时间,视觉体验提高了很多。

PWA 直出

-w500

针对上述、常见的直出应用来讲,咱们可以优化的点在哪里呢?让咱们来详细分析一波,这也是今天咱们要给你们分享的重点。

首先看看直出应用各个环节的耗时表 (本地环境 2018款 iMac):

过程名称 过程花费
Node 内 CGI 拉取 300 ms
RenderToString 20 ms
网络耗时 10 ms
前端HTML渲染 30 ms

从上面的表中咱们看出,直出渲染的耗时的大头仍是在 CGI 接口的拉取上。

咱们如今提出两个问题

  • CGI 接口的数据是否能够缓存 ?
  • HTML 又是否能够缓存 ?

1、接口的动静分离

-w300

这个页面的接口数据中,有一些数据,是实时变更的, 好比:当前还剩多少个名额、此时此刻课程的价格、用户是否购买过这个课程等。

这些数据的特性决定了这个数据接口不可以被缓存。(假设将其缓存,那么就会存在可能用户进来看到当前还剩下10个名额,其实课程已经卖光了的状况)

为了这个时间耗时的大头,咱们作了CGI接口的动静分离

将与用户态、当前时间没有关联的数据(好比课程标题课程上课的时间试听模块的地址等)放在一个接口(静态接口),其余变化的数据放在另外一个接口(动态接口)。

那么可使用静态的接口来作服务端渲染,好处是第一比较快(少了动态的信息,并且后台也能够作缓存),第二 Node 直出能够作缓存了。

2、直出 Redis 缓存

这样咱们就能够将那部分静态的、不会常常变更的数据用来直出 HTML,而后将这个 HTML 文件缓存到 Redis 中

客户端请求此网页,Node 端接受到请求以后,先去 Redis 里拿缓存的 HTML,若是 Redis 缓存没有命中,则拉取静态的 CGI 接口渲染出 HTML存入 Redis。

客户端拿到 HTML 以后,会马上渲染,而后再用 JS 去请求动态的数据,渲染到相应的地方。

作完以后咱们能够看到优化效果的提高是很是很是明显的:

直接从 262ms 提高到了 16ms !(本地环境),简直飞通常的感受,妈妈不再用担忧领导看耗时了。

3、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 直出;右 离线包):

duibi

从上图能够看出,使用了 PWA 直出缓存以后,首屏渲染基本是毫秒开,能够说与 Native 并肩了。

通过咱们的数据测试,使用 PWA 直出缓存,首屏渲染的时间最好能够到400ms左右级别:

PWA 直出细节优化

1、防页面跳动

由于对接口进行了动静分离,使用静态接口直出页面,而后在客户端拉取动态数据渲染完。这就可能会致使页面的抖动(好比详情页中的试听模块,是在客户端渲染的)。

由于高度改变了,视觉上就会出现抖动(具体能够参考上面章节直出时候的 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 以前。

-w500

防抖动优化效果以下 (左优化完,右未优化):

duibi_doudong

2、冷启动预加载

虽然咱们作了 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 直出遗留问题

1、兼容性问题

随着 PWA 技术的发展,现今大部分手机以及 PC 环境已经支持对 PWA 进行了支持。通过咱们的测试发现:安卓基本上都是支持的,IOS 须要11.3以上才支持。

Service Workers 兼容性图

具体的兼容性支持点我查看

2、IOS 渲染问题

不少的经验告诉咱们,外联的 script 标签要放在 body 的后面,由于它会阻塞页面的 DOM 渲染。

通过测试发现,IOS 的 WebView (UIWebView)渲染机制并不会上述同样,而是要等到后面的 JS 执行完以后才渲染页面,若是是这样,咱们的直出渲染优化就没有效果了(由于 HTML 并不在最开始渲染),这里可使用 script 标签的 asyncdefer 属性来达到异步渲染的做用。

升级 WkWebView 以后,状况获得改善,渲染正常。

附录

参考资料

更多基于 PWA 的性能优化实践,请查看 IMWeb 团队刘华的分享

关于咱们

IMWeb 团队隶属腾讯公司,是国内最专业的前端团队之一。

咱们专一前端领域多年,负责过 QQ 资料、QQ 注册、QQ 群等亿级业务。目前聚焦于在线教育领域,精心打磨 腾讯课堂企鹅辅导 两大产品。

现团队有大量 HC,欢迎对技术有着强烈兴趣的你来加入咱们,和咱们一块儿在前端的世界里愉快地玩耍, Work hard,Play hard。

简历投递: jaxjiang@tencent.com