Tinder渐进式网页应用性能案例学习

原文出处 A Tinder Progressive Web App Performance Case Study

Tinder最近对移动端“右滑”了。他们最近出品的响应式的渐进式网页应用——Tinder Online——已经可以在桌面和移动端使用了,应用采用了新技术做JavaScript性能优化,并用Service WorkersPush Notification分别做了网络弹性和对话约会。今天我们来简单过一遍他们的性能学习之路。

踏上渐进式网页应用的旅程

Tinder Online的目标是在新市场站稳脚跟,力争达到在其它平台上Tinder V1版本的使用体验。

PWA的MVP花了三个月,采用了React构架UI库和Redux做状态管理。这些努力的结果就是,渐进式网页应用只使用了10%的流量消耗就达到了Tinder核心的应用体验,这对流量费用昂贵或连接速度慢的地区的人来讲尤为重要:

上图是Tinder Online和原生应用的流量消耗对比。值得注意的是,这并不是横向的比较。PWA只会按需从新路由上加载代码,这些额外的代码加载分散在应用的整个生命周期里面。后续导航消耗流量依旧比下载整个app的要少。

早期征兆显示,对比原生应用,PWA表现出了流畅的滑动体验,更多的消息操作和更长的会话时间。在PWA上:

  • 用户的滑动操作更多
  • 用户之间发送的消息更多
  • 用户的购买量与原生应用持平
  • 用户对个人资料的编辑更为频繁
  • 用户的会话时间更长

性能

Tinder Online的移动端用户使用最多的设备包括:

  • Apple iPhone和iPad
  • Samsung Galaxy S8
  • Samsung Galaxy S7
  • Motorola Moto G4

通过使用Chrome用户体验报告 (CrUX),我们了解到大部分的用户在浏览Tinder Online的时候使用的是4G网络:

注:Rick Viscomi最近在PerfPlanet中加入了CrUX,Inian Parameshwaran使用了rUXt将数据变得更加可视化。

在使用WebPageTestLighthouse(4G下使用Galaxy S7)测试后,我们看到用户在五秒钟之内即可加载完毕并进入可交互状态。

当然,在受CPU约束的中端移动设备(比如Moto G4)上,仍然存在很多可优化空间:

Tinder正在努力优化他们的体验,未来我们也希望能看到他们在网页性能优化上所做的工作。

性能优化

Tinder使用了很多技术来提升加载速度和减少进入可交互状态之前的时间。他们使用了基于路由的代码分割,并引入了性能预算和资源的长期缓存。

基于路由的代码分割

Tinder网页端最初包含了庞大的JavaScript代码包,这延长了应用的加载时间。这些包中包含了很多并不需要立即加载的代码,因此这些代码可以通过使用代码分割打碎。只加载用户首屏使用的代码,其它的在需要的时候懒加载,这种办法非常有用。

为了达到这一点,Tinder使用了React RouterReact Loadable。他们的应用把路由和渲染信息集中在了一个配置里面,他们发现可以直接在顶层做代码分割。

简介: React Loadable的的作者是James Kyle,他的初衷是简化以组件为中心的React应用的代码分割Loadable是一个高阶组件(一个创建组件的函数),能够在组件层使得分割代码包更加简单。

比如我们有两个组件,“A”和“B”。在做代码分割之前,Tinder静态地将所有东西(A、B等等)都引入到主包里面。这种方式很低效,因为我们并不立刻且同时需要A和B两个组件:

在做了代码分割之后,组件A和组件B会在需要的时候再加载。Tinder在JS中引入了React Loadable,动态导入webpack的魔法注释(针对命名动态代码块):

针对“vendor”(库),Tinder使用了CommonsChunkPlugin将频繁使用的公共库单独打包,这样可以有效利用长缓存:

接着,Tinder使用React Loadable的预加载支持来预加载下一页可能会使用到的资源:

Tinder也使用了Service Workers来预缓存所有路由层的代码包,并把用户最有可能访问的路由在未做分割的情况下加进了主包中。当然他们也使用了最常用的优化手段,例如通过UglifyJS最小化JavaScript文件的体积:

new webpack.optimize.UglifyJsPlugin({
    parallel: true,
    compress: {
    warnings: false,
        screw_ie8: true
    },
    sourceMap: SHOULD_SOURCEMAP
}),

影响

在引入了基于路由的代码分割之后,他们的主包的大小从166KB降至了101KB,DCL从5.46秒降至4.69秒:

资源的长期缓存

通过使用webpack的[chunkhash]向文件名中加入独一无二的字段,这保证了静态资源的长期缓存

Tinder在依赖中使用了很多开源的库。vendor的[chunkhash]会随着对这些库作出的改变而改变,这就会使缓存失效。为了解决这个问题,Tinder定义了一个外部库白名单,并将他们的manifest文件从主代码块中分离出来,以改善缓存。现在两个代码包的大小大约都是160KB。

预加载后期使用资源

作为一名新手,<link rel="preload">是一个声明式的命令,指导浏览器预先加载关键的、后续会用到的资源。在一个单页面应用里面,这些资源可能会是JavaScript包。

Tinder启用了对对用户体验至关重要的JavaScript/webpack包的预加载的支持。这将加载时间减少了1秒,初次绘制时间也从1000毫秒降至500毫秒左右。

性能预算

Tinder采用了性能预算来帮助他们在移动端达到他们的目标。正如Alex Russell在“Can you afford it?: real-world performance budgets”提及的,在使用3G网络的中端移动设备上,你所能利用的资源是非常有限的。

为了保证快速的交互,Tinder制定了一个强制性的体积预算——主包和vendor包155KB,异步加载(懒加载)55KB,其它的块35KB。CSS也有限制,20KB。如果他们想将不同设备上的性能保持一致,这些至关重要。

Webpack打包分析

Webpack打包分析器可以绘制JavaScript包的依赖关系图,可以使你更容易地发现性能瓶颈,以便继续做优化。

Tinder使用了Webpack Bundle Analyzer发现了很多可以继续优化的地方:

  • Polyfills:Tinder的目标是现代浏览器,但他们同样兼顾IE11和Android 4.4设备及以上的浏览器,为了使polyfills和编译后的代码体积尽可能小,他们使用了babel-preset-env以及core-js**。

  • 削减库的使用:Tinder直接使用IndexedDB替代localForage

  • 更好的分割:将首屏/首次交互不需要的组件从主包中分离出去。

  • 代码复用:将使用次数超过三次的模块提取出来,创建为异步加载的公共模块。

  • CSS:Tinder将关键CSS从核心包中分离了出来(因为他们使用了服务端渲染,无论如何都将这部分CSS发回到客户端)

使用打包分析器同样使Tinder意识到Webpack的Lodash Module Replacement Plugin的重要性。这个插件可以使用更简单的替换包,创建出更小的Lodash构建包:

Webpack打包分析器可以嵌入到你的Webpack配置中。Tinder的设置看起来就像下面的这样:

plugins: [
    new BundleAnalyzerPlugin({
        analyzerMode: 'server',
        analyzerPort: 8888,
        reportFilename: 'report.html',
        openAnalyzer: true,
        generateStatsFile: false,
        statsFilename: 'stats.json',
        statsOptions: null
    })
]

剩下的大部分JavaScript就是主包,如果不对Redux Reducer和Saga Register做架构上的改变的话,很难再去做这部分的分割工作。

CSS策略

Tinder使用了Atomic CSS来创建高度可复用的CSS样式。所有的这些原子化的CSS样式会在初次绘制时加入到内联样式中,剩下的CSS则会在样式表中加载(包括动画和基础/重置样式)。关键的样式有20KB的限制,最近的打包体积已经降低到了11KB以下。

Tinder使用CSS stats和Google Analystics来分析每个发行版本中改动的地方。在采用Atomic CSS之前,页面的平均加载时间是6.75秒,之后则降低为5.75秒。

Tinder Online同样使用了PostCSS Autoprefixer plugin解析CSS,并按照Can I Use的规则为CSS加上前缀。

new webpack.LoaderOptionsPlugin({
    options: {
        context: paths.basePath,
        output: { path: './' },
        minimize: true,
        postcss: [
            autoprefixer({
            browsers: [
                'last 2 versions',
                'not ie < 11',
                'Safari >= 8'
                ]
            })
        ]
    }
}),

运行时性能

使用requestIdleCallback()延迟非关键工作

为了提高运行时的性能,Tinder选择了使用requestIdleCallback()将非关键动作推迟到空闲时间中。

requestIdleCallback(myNonEssentialWork);

其中包括诸如instrumentation beacons这样的工作。他们也对HTML复合层做了简化处理,以减少滑动时的绘制次数。 在滑动时对instrumentation beacons使用requestIdleCallback():

之前..

之后

依赖升级

Webpack 3 + 作用域提升 在旧版本的webpack中打包时,每个模块都会被包含在一个单独的闭包中。这些包含函数会拖慢浏览器中JavaScript的执行速度。Webpack 3引入了“作用域提升”——可以将所有的模块打包进一个闭包之中,这样可以使浏览器中JavaScript的执行速度更快。这个功能使用的是Module Concatenation plugin:

new webpack.optimize.ModuleConcatenationPlugin()

Webpack 3的作用域提升将Tinder的vendor包的初次解析时间降低了8%。

React 16

React16相比之前的版本,做了一些改进,降低了React包的体积大小。这去除了现在已经不再使用的代码,带来了更好的打包体验(使用Rollup)。

通过将React 15升级为React 16,Tinder将gzip压缩后的vendor包的体积减小了7% react + react-dom的体积过去是50KB,现在已经降低为35KB。这要感谢Dan AbramovDominic GannawayNate Hunzaker,他们在这项工作中起到了指导性的作用。

Workbox——网络弹性和离线资源缓存

Tinder还使用了Workbox Webpack plugin,来缓存应用骨架和诸如主包、vendor包、manifest包以及CSS等的核心静态资源。针对频繁的访问,这保证了网络弹性,并且确保用户在回到应用时的加载速度会更快。

机遇

使用source-map-explorer(另一个包分析器)对Tinder包做深入分析之后,我们发现仍然存在很多机会可以减小应用的体积。在登录之前,Facebook Photos、推送消息和验证码就已经获取了。这些如果从关键路径中移除,还可以将主包体积降低20%:

关键路径中的另一个依赖则是Facebook SDK脚本,体积为200KB。丢掉这个(可以在需要的时候懒加载)可以节省1秒的初次加载时间。

结语

Tinder仍然在迭代他们的渐进式网页应用,但是已经从他们的工作中看到了很多好的结果。赶快去浏览Tinder.com,跟上他们未来的进度吧!

恭喜Roderick Hsiao、Jordan Banafsheha和Erik Hellenbrand上线Tinder Online,感谢他们为这篇文章的付出。并衷心感谢Cheney Tsai为这篇文章做的审校工作。