编者按:本文由INCHMAN在众成翻译平台上翻译。前两天苹果 Safari 团队宣布Safari 11.1正式版将跟随iOS 11.3和macOS 10.13.4一起发布,届时将默认支持Service Worker,同时支持还Web App Manifest以及支持直接添加Web应用到主屏。如果你还不了解PWA,那么请跟随今天的案例,看看生产环境下如何优化性能,同时渐进式地增强我们的应用:
Tinder最近对移动端“右滑”了。他们最近出品的响应式的渐进式网页应用——Tinder Online——已经可以在桌面和移动端使用了,应用采用了新技术做JavaScript性能优化,并用Service Workers来适应多变的网络环境,用Push 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将数据变得更加可视化。
在使用WebPageTest和Lighthouse(4G下使用Galaxy S7)测试后,我们看到用户在五秒钟之内即可加载完毕并进入可交互状态。
当然,在受CPU约束的中端移动设备(比如Moto G4)上,仍然存在很多可优化空间:
Tinder正在努力优化他们的体验,未来我们也希望能看到他们在网页性能优化上所做的工作。
性能优化
Tinder使用了很多技术来提升加载速度和减少进入可交互状态之前的时间。他们使用了基于路由的代码分割,并引入了性能预算和资源的长期缓存。
基于路由的代码分割
Tinder网页端最初包含了庞大的JavaScript代码包,这延长了应用的加载时间。这些包中包含了很多并不需要立即加载的代码,因此这些代码可以通过使用代码分割打碎。只加载用户首屏使用的代码,其它的在需要的时候懒加载,这种办法非常有用。
为了达到这一点,Tinder使用了React Router和React 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。
预加载后期使用资源
作为一名新手,是一个声明式的命令,指导浏览器预先加载关键的、后续会用到的资源。在一个单页面应用里面,这些资源可能会是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 Abramov,Dominic Gannaway和Nate Hunzaker,他们在这项工作中起到了指导性的作用。
Workbox——网络弹性和离线资源缓存
Tinder还使用了Workbox Webpack plugin,来缓存应用骨架和诸如主包、vendor包、manifest包以及CSS等的核心静态资源。针对频繁的访问,这保证了网络弹性,并且确保用户在回到应用时的加载速度会更快。
机遇
使用source-map-explorer(另一个包分析器)对Tinder包做深入分析之后,我们发现仍然存在很多机会可以减小应用的体积。在登录之前,Facebook Photos、推送消息和验证码就已经获取了。这些如果从关键路径中移除,还可以将主包体积降低20%:
关键路径中的另一个依赖则是Facebook SDK脚本,体积为200KB。丢掉这个(可以在需要的时候懒加载)可以节省1秒的初次加载时间。
结语
Tinder仍然在迭代他们的渐进式网页应用,但是已经从他们的工作中看到了很多好的结果。赶快去浏览Tinder.com,跟上他们未来的进度吧!
奇舞周刊
——————————————————
领略前端技术 阅读奇舞周刊
长按二维码,关注奇舞周刊
▼