向白

Pinterest 渐进式网页应用及性能问题

原文链接: medium.com

Pinterest 新的移动端网页采用了 Progressive Web App。在这篇文章中,我们将介绍他们的一些工作,通过保持JavaScript包的精简以及采用 Service Workers 来应对网络波动。

在你的手机上登录 https://pinterest.com 体验他们新的移动端网站

为什么是渐进式网页应用 (PWA)? 和一些历史。

Pinterest 决定使用开始使用 PWA 是因为他们专注于国际增长,他们把目光放到了移动端网页。

在分析了使用了移动端网页未登录用户的数据后,他们意识到自己古老,缓慢的网页体验仅仅转化了 1% 的用户去注册,登录或下载安装原生应用。这是大大提高转化率的机会,所以他们投资了 PWA。

在一个季度中打造并且推出 PWA

在过去的三个月中,Pinterest 使用 React,Redux,webpack 重新打造了他们移动端网页。他们的移动端网页重写使核心业务指标的一些积极改进。

与旧的移动Web体验相比,花费的时间是40%,用户产生的广告收入增加了 44% ,核心业务增加了 60%

他们移动端网页的重写同时也提高了性能。

在移动端3G网络下平均加载速度更快

Pinterest 旧版的移动端网页体验像一块巨石—它包含了大量 CPU 计算的 JavaScript 包,拖延了 Pin 页面的加载和可交互的速度。

在UI界面可响应之前,用户通常需要等待 23秒

Pinterest 旧版的移动端网站需要花23秒才可以交互,浏览器需要接收大约 2.5 MB 的 JavaScript (主包大约1.5MB,懒加载大约1MB) 并且花数秒钟去解析和编译在主线程可响应之前。

他们新版移动端网页体验猛增。

他们不仅分散和削减了数百KB的JavaScript,将其核心包的大小从650KB降低到150KB,而且还改进了关键性能指标。 首次有意义的渲染从4.2s下降到1.8s,可交互时间从23s减少到5.6s。

这是Android设备在使用缓慢的3G网络连接。 重复访问,情况更好。

由于Service Worker对主要JavaScript,CSS和静态UI资源进行缓存,这样能够将重复访问的互动时间缩短至3.9秒:

虽然 Pinterest 提供 iOS 和 Android 应用程序,但是它们能够提供在网页上相同的核心主页消息流订阅体验,只需要一小部分的前期下载成本 - 压缩后只需约150KB。 这与 Android 的 9.6MB 和 iOS 的 56MB 提供相同的体验恰恰相反:

然而,这里要指出的是,这不是苹果设备之间的比较。PWA根据需要加载新路由的代码,附加代码的成本在应用程序的生命周期内分摊。随后的导航仍然不会像下载应用程序那样花费太多的流量。

Pinterest 渐进式网页应用在移动端的 Firefox, Edge 和 Safari 上。

基于路由的 JavaScript 打包组合

只需加载用户需要的代码,就可以快速获取网页来加载和获取交互。 这减少了网络传输和JavaScript解析/编译时间。 非关键资源可以根据需要延迟加载。

Pinterest开始打破他们的几兆字节的JavaScript包,将其分成三个不同类别的webpack块,他们工作得很好:

  • 渲染块包含额外的依赖(react, redux, react-router, etc) ~ 73kb

  • 入口块包含渲染网页的主要代码(举例:通用模块,页面的外壳,redux 的 store) ~ 72KB

  • 异步 路由块包含单独路由的代码 ~13–18KB

网络瀑布的高亮显示了该如何逐步优化代码,从而避免了巨石般的打包:

对于长期缓存,Pinterest也为每个文件名使用块特定的散列。

Pinterest 使用 webpack 中的 CommonsChunkPlugin 来分离出他们的可缓存的 vendor 包:

他们也结合 code-splitting 使用 React Router

对需要支持的浏览器使用 babel-preset-env 转换代码

Pinterest 使用 Babel babel-preset-env 转换所需支持的现代浏览器中不支持的 ES2015+ 特性。Pinterest 支持现代浏览器的最新两个版本,他们的 .babelrc 是这个样子的:

还有进一步的优化,他们可以做,只有条件地满足需要的 polyfills (例如 Internationalization API ),但这是未来的计划。

使用Webpack Bundle Analyzer分析改进空间

Webpack Bundle Analyzer 是一个非常棒的工具,它能真正的了解你发给用户 JavaScript 包之间的依赖关系。

下面,你能看到很多紫色,粉色和蓝色的块,它们在 Pinterest 中是最早加载输出的。这些是用于延迟加载路由的异步块。Webpack Bundle Analyzer 允许可视化大部分这些块包含重复的代码

Webpack Bundle Analyzer 帮助可视化所有块之间的这个问题的比例。

Pinterest 使用块中的重复代码信息进行调用。他们将异步块中的重复代码移动到其主块。 它将入口块的大小增加了20%,但将所有延迟加载块的大小减少了90%!

图片优化

Pinterest PWA中大多数内容的延迟加载是由无限的 Masonry 网格来处理的。 它内置了对虚拟化的支持,并且只挂载在视口中的子项。

Pinterest 也使用渐进式加载技术来处理 PWA 中的图像。 占位符在初始化每个 Pin 时占主导地位。 Pin 的图像使用 Progressive JPEGs(渐进式JPEG),可在每次扫描时提高图像质量:

React 性能痛点

Pinterest 用 React 处理了一些渲染性能问题, Masonry 网格作为他们的一部分。 安装和卸载组件(如 Pins)的大树可能会很慢。Pin 中有很多东西:

虽然在写 Pinterest 的时候正在使用 React 15.5.4,但是他们希望 React 16 (Fiber)能够减少花费在卸载上的时间。 与此同时,虚拟化网格显着帮助了组件卸载时间。

Pinterest还会限制 Pins 的插入,以便他们可以更快地测量/渲染第一个 Pins,但意味着设备CPU的整体工作量更大。

导航过渡

为了提高感知性,Pinterest 也更新独立于路由的导航栏图标的选定状态。 这使得从一个路由到另一个路由的导航不会因网络阻塞而感觉到缓慢。 用户在等待数据到达时快速获取可视化界面:

使用 Redux 的经验

Pinterest 对所有 API 数据使用 normalizr (它根据模式规范化嵌套JSON) 。可以使用 Redux DevTools 中看到他们。

这个过程的缺点是非规范化是缓慢的,所以他们最终严重依赖 reselect 的选择器模式来记忆渲染过程中的非规范化。 它们也总是在尽可能低的层次上进行非规范化处理,以确保单个更新不会导致大量的重新渲染。

举个例子,他们的网格项目列表只是 Pin ID 与 Pin 组件非正常化本身。 如果任何给定 Pin 有变化,则完整的网格不必重新渲染。 权衡是在Pinterest PWA 中有很多 Redux 订阅,尽管这没有导致明显的性能问题。

通过 Service Workers 缓存资源

Pinteres t使用 Workbox 库来生成和管理他们的 Service Workers:

今天,Pinterest 使用缓存优先策略缓存任何 JavaScript 或 CSS 包,并缓存其用户界面(应用程序外壳)。

缓存优先设置中,如果请求与缓存条目匹配,则以此作为响应。 否则,尝试从网络获取资源。 如果网络请求成功,更新缓存。 要了解更多有关使用 Service Worker 的缓存策略,请阅读 Jake Archibald’s Offline Cookbook 的离线手册。

它们为应用程序外壳(webpack的运行时,vendor 和入口块)加载的初始包定义了预缓存。

由于 Pinterest 是具有全球影响力的网站,支持多种语言,因此它们还会生成每个语言环境的 Service Worker 配置,以便它们可以预先缓存语言环境包。 Pinterest 也使用 webpack 的命名块预缓存顶级异步路由包。

这项工作是在几个较小的迭代中推出的。

  • 首先:Pinterest 的 Service Worker 只会在运行时缓存按需延迟加载的脚本。 这是利用V8的代码缓存,帮助跳过重复视图的一些分析/编译成本,使他们可以加载更快。 从 Service Worker 存在的高速缓存存储器提供的脚本可以及时的选择代码高速缓存,因为浏览器可能知道用户最终将在重复视图中使用这些资源。

  • 之后,Pinterest 进行 预缓存他们的 vendor 和 入口块

  • 接下来,Pinterest 开始 预缓存一些常用路由(像 主页, pin 页面,搜索页面 等)

  • 最后,他们开始为每个语言环境生成一个 Service Worker,以便他们也可以缓存语言环境包。 这对于重复加载性能非常重要,同时也为大多数用户提供了基本的离线渲染功能。

应用外壳的挑战

Pinterest发现实施他们的应用程序壳有点棘手。 由于桌面时代的假设,有多少数据可以通过有线连接发送,初始的有效负载很大,包含了很多非关键信息,如用户的实验组,用户信息,上下文信息等。

他们不得不自问:“我们是否将这些内容缓存在应用程序外壳中? 或者在渲染任何内容之前,先阻塞网络请求来获取它”。

他们决定将其缓存在应用程序外壳中,这需要管理什么时候使应用程序外壳失效(注销,从设置等更新用户信息)。每个请求的响应都包含 appVersion — 如果应用程序版本发生变化,他们将取消注册 Service Worker,注册新的服务,然后在下一次路由更改时重新加载整个页面。

将这些信息添加到应用程序外壳是有点棘手,但为了避免渲染被请求阻塞,是值得的。

审计 和 Lighthouse

Pinterest 使用 Lighthouse 来验证他们的性能改进是正确的。 观察诸如“持续互动时间”等指标非常有用。

明年,他们希望使用 Lighthouse 作为回归机制来验证页面加载速度是否保持快速。

新特性

Pinterest 刚刚部署了支持 Web 推送消息的版本,并且在未登录(注销)的状态下的 PWA 也可以正常使用。

他们有兴趣探索支持 <link rel=preload> 预加载关键包并减少首次加载时向用户提供的未使用JavaScript的数量。 请继续关注未来更多精彩的演出!

-----本文译者向白