(可在手机上登录 https://pinterest.com 去体验下 Pinterest 新的移动端网站)
在最开始的时候,因为专注于国际市场的增长,Pinterest 关注移动端网页的开发,也由此有了 Pinterest PWA。
在分析了未经验证的移动端网页用户的相关数据后,Pinterest 发现他们原来旧而慢的网络体验仅能将 1% 的用户转化为注册、登录或下载 app 作为本地应用使用的用户。如果能够提升这一转化率的话,无疑是一个巨大的机会,所以他们开始了对 PWA 的投资。
在一个季度内建立和推出 PWA
用时超过 3 个月,Pinterest 通过使用 React、Redux 和 webpack 重构了他们移动端网页的体验。移动端网页的重写也提高了他们几项核心业务指标。
与旧移动端网页的体验相比,新移动端网页用户的使用时间增加了 40%,用户生成的广告收益增加了 44%,并且核心业务增长了 60%。
与此同时,移动端网页的重写也改善了 Pinterest 网页的一些性能。
Pinterest 旧的移动端网页含有大量的需要占用很多 CPU 的 JavaScript 包,延长了 Pin 网页加载和取得互动所需的时间。
在可以进行任何互动之前,用户经常需要等 23 秒:
(Pinterest 原有的移动端网站需要花费 23s 才能开始互动。这一过程中,他们会发送 2.5MB 以上的 JavaScript,其中约有 1.5MB 用于主包,1MB 用于懒加载。在主线程最终能够实现交互之前,需要花费几秒钟的时间来解析和编译)
他们新移动端网页的体验有了极大的提高。
不仅是因为他们分散和减少了数百 KB 的 JavaScript,将核心包体的大小从 650KB 降到了 150KB,也是因为他们提高了网页的一些关键性能指标。首次有效绘制时间由 4.2s 降低到了 1.8s,并且可交互时间由 23s 降低到了 5.6s。
以上的测试结果是在连接了缓慢 3G 网络的普通 Android 硬件上得到的。在重复访问的情况下,结果甚至更好。
得益于 Service Worker 缓存了主要的 JavaScript、CSS 和静态 UI 资源,重复访问的时间被缩短到了 3.9s:
尽管 Pinterest 有 iOS 和 Android 应用,但是只需在开始时下载约为 150KB 优化压缩(minified & gzipped)过的代码,就能够在网页应用上实现与本地应用相同的主页推送体验。对比于 Android 版应用的 9.6MB 和 iOS 版应用的 56MB:
然而值得注意的是,与本地应用相比 Pinterest PWA 的优点并不局限于前期主页推送体验。PWA 还会按新路由的需要来加载代码,而且额外代码的成本会被分摊到使用网页应用的整个过程中。随后的导航仍然不会像下载应用那样消耗大量的数据。
(Pinterest 的 PWA 分别在移动端的 Firefox、Edge 和 Safari 上的显示)
Pinterest 开始将原有的高达几个 MB 的 JavaScript 包拆分成 3 种不同类型的 webpack 模块,效果还挺不错:
一类是包含外部依赖性的 vendor 模块(react、redux、react-router 等),大约 73KB
一类是包含渲染应用所需要的大部分代码的入口模块(entry chunk)(即常见的库,主要的页面外壳,我们的 redux store),大约 72KB
一类是包含关于单个路由的代码的异步路由模块(async route chunk),大约 13 到 18KB
以下 Network 的瀑布记录,突出显示了渐进式地按需传送代码如何避免了整体(monolithic)传送包体的需求:
(对于长期缓存,Pinterest 也在每个文件名中包含了一个模块相关 (chunk-specific) 的哈希,通过 chunkhash 替换)
Pinterest 用了 webpack 的 CommonsChunkPlugin 插件来将他们的 vendor 包体拆分到可缓存的模块内:
const bundles = {
'vendor-mweb': [
'app/mobile/polyfills.js',
'intl',
'normalizr',
'react-dom',
'react-redux',
'react-router-dom',
'react',
'redux'
],
'entryChunk-webpack': 'app/mobile/runtime.js',
'entryChunk-mobile': 'app/mobile/index.js'
};
const chunkPlugins = [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor-mweb',
minChunks: Infinity,
chunks: ['entryChunk-mobile']
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'entryChunk-webpack',
minChunks: Infinity,
chunks: ['vendor-mweb']
}),
new webpack.optimize.CommonsChunkPlugin({
children: true,
name: 'entryChunk-mobile',
minChunks: (module, count) => {
return module.resource && (isCommonLib(resource) || count >= 3);
}
})
];
在分块的过程中,他们也用了 React Router 来实现代码拆分:
// Create a loader
const Closeup = () => import(/* webpackChunkName: "CloseupPage" */ 'app/mobile/routes/CloseupPage');
// Register it to the route
route('/pin/:pinId', routes.Closeup, { name: 'Closeup' }),
// Render a react-router-v4 Route with the route bundle loader
<Route exact key="matched-route" path={path} render={matchProps =>
<PageRoute
bundleLoader={loader}
routeName={name}
{...matchProps}
{...props}
/>}
/>
// Async load the route bundle
class PageRoute extends PureComponent {
render() {
const { bundleLoader, ...props } = this.props;
return <Loader loader={bundleLoader} {...props} />;
}
}
// Load it and render
class Loader extends PureComponent {
componentWillMount() {
this.props.loader().then(module => {
this.setState({ LoadedComponent: module.default });
});
}
}
Pinterest 用了 Babel 的 babel-preset-env 来仅编译(transpile)不受目标浏览器支持的 ES2015+ 功能。Pinterest 针对的是现代浏览器最新的两个版本,他们的.babelrc 设置类似于:
{
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions"]
}
}]
]
}
其实 Pinterest 也可以对此作进一步的优化,按照实际需要有条件地提供 polyfills(比如:Safari 国际化的 API)。但是目前这还是这一优化仍在计划中。
Webpack Bundle Analyzer 是一个很好的工具,可以帮助人切实地理解传送给客户的 JavaScript 包之间的依赖关系。
如下图所示,在早期的 Pinterest 版本的输出中,有很多的紫色,粉色和蓝色的区域。这些都是被懒加载的路由异步模块。Webpack Bundle Analyzer 可以帮助 Pinterest 将大多数的含有重复代码的模块可视化:
此处输入图片的描述
Webpack Bundle Analyzer 可以将重复代码在不同模块之间的大小比例视觉化。
在有了所有模块中有重复代码的信息之后,Pinterest 就可以做出调用。他们把异步模块中的重复代码移到了主要模块中。虽然这一改动增加了 20% 入口模块的大小,但是却将所有懒加载模块的大小减小了 90%!
大部分 Pinterest PWA 中内容的懒加载都是通过无限网格瀑布流插件 Masonry 来处理的。它内置了对虚拟化的支持,并且仅装载(mounting)视口内的子项。
Pinterest 也在他们的 PWA 中使用了渐进式加载图片的技术。有主导颜色的占位符在最开始会被用于每一个 Pin。而 Pin 的图像会以 Progressive JPEGs 来提供,其质量会随着扫描次数的增加而增加:
在 Pinterest 使用网格瀑布流 Masonry 插件的同时,他们也面临着 React 带来的一些渲染性能的问题。装载和卸载大的组件树(像 Pin)可能会很慢。一个 Pin 里面有很多的东西:
尽管当时他们写 Pinterest 的时候用的是 React 15.5.4, 但是他们寄希望于 React 16(Fiber) 将会大大减少卸载所用的时间。与此同时,虚拟化的网格也会显著地减少组件卸载的时间。
Pinterest 还会限制 Pin 的插入,以便更快地测量 / 渲染第一个 Pin,但是这也意味着设备 CPU 的工作量更大了。
为了提高感知性能,Pinterest 也更新了导航栏图标的选定状态,将其独立于路由之外。这就确保了当导航从一个路由转到另一个路由的时候,用户并不会因为网络的阻塞而感到缓慢。用户在等待数据到达时可以快速地获得可视化界面。
Pinterest 在他们所有的 API 数据中均使用了 normalizr(normalizr 会根据一种模式来规范化嵌套的 JSON)。从 Redux DevTools 就可以看出:
这样做的缺点是逆规范化 (denormalization) 会变得很慢,在渲染的阶段最终他们很大程度上是依赖于 reselect 的 selector 模式来记忆(memoizing)逆规范化。他们也尽可能的在最低程度上进行逆规范处理,以确保单个的更新不会导致大规模的重新渲染。
举个例子来说,他们的网格项目列表只是由 Pin ID 与逆规范化自身的 Pin 组件组成的。如果任何给定的 Pin 有了改变,则完整的网格不必重新渲染。但是有得就有失,这样 Pinterest PWA 就有了很多 Redux 用户,虽然这一点尚未对性能产生显著的影响。
Pinterest 用了 Workbox 库来生成和管理他们的 Service worker:
/* global $VERSION, $Cache, importScripts, WorkboxSW */
importScripts('https://unpkg.com/workbox-sw@1.1.0/build/importScripts/workbox-sw.prod.v1.1.0.js');
// Add app shell to the webpack-generated precache list
$Cache.precache.push({ url: 'sw-shell.html', revision: $VERSION });
// Register precache list with Workbox
const workbox = new WorkboxSW({ handleFetch: true, skipWaiting: true, clientClaim: true });
workbox.precache($Cache.precache);
// Runtime cache all js
workbox.router.registerRoute(/webapp\/js\/.*\.js/, workbox.strategies.cacheFirst());
// Prefer app-shell for full-page loads
workbox.router.registerNavigationRoute('sw-shell.html', {
blacklist: [
// bunch of non-app routes
],
});
如今,Pinterest 使用缓存优先策略(cache-first strategy)来缓存任何 JavaScript 或者 CSS 的包,并且也会缓存其用户的界面(应用程序的外壳)。
(在缓存资源优先的设置中,如果请求与缓存条目相匹配,则以缓存的资源为准。否则,则尝试从网络获取资源。如果网络请求成功,则对缓存进行更新。要了解更多有关使用 Service Worker 的缓存策略,请阅读 Jake Archibald 的 Offline Cookbook。)
他们也为应用程序外壳(webpack 运行时,vendor 和 entry 模块)加载的初始包定义了预缓存。
因为 Pinterest 是一个具有全球影响力的网站,能够支持多种语言,所以他们还会生成适用于每个语言区域的 Service Worker 配置,以便其预缓存不同语言区域的软件包。Pinterest 也使用了 webpack 的命名模块来预缓存顶级(top-level)异步路由包。
这项工作是在几个较小的迭代中逐步推出完成的。
第一步:Pinterest 的 Service Worker 仅缓存运行时需要懒加载的脚本。充分利用 V8 的代码缓存,跳过了一些在重复视图解析 / 编译所需的成本,使得加载能够快速的进行。从有 Service Worker 存在的 Cache Storage 获得的脚本能够很快地进行代码缓存,因为浏览器很可能知道当重复访问时用户最终会重复使用这些资源。
在这之后,Pinterest 推进到预缓存其 vendor 和入口模块。
接下来,Pinterest 开始预缓存一些使用最多的路由(比如主页,锁定收藏的网页,搜索页等)
最后,他们开始为每个地域生成一个 Service Worker,这样的话就能够缓存不同地域的语言包。这不仅是为了保证重复加载的性能,也是为了保证绝大多数的用户可以享受基本的离线渲染功能。
/* Create a service worker for every locale to precache the locale bundle */
const ServiceWorkerConfigs = locales.reduce((configs, locale) => {
return Object.assign(configs, {
[`mobile-${locale}`]: Object.assign({}, BaseConfig, {
template: path.join(__dirname, 'swTemplates/mobileBase.js'),
cache: {
template: path.join(__dirname, 'swTemplates/mobileCache.js'),
precache: [
'vendor-mweb-.*\\.js$',
'entryChunk-mobile-.*\\.js$',
'entryChunk-webpack-.*\\.js$',
`locale-${locale}-mobile.*js$`,
'pjs-HomePage.*\\.js$',
'pjs-SearchPage.*\\.js$',
'pjs-CloseupPage.*\\.js$'
]
}
})
});
}, {});
// Add to webpack
plugins: [
new ServiceWorkerPlugin(BaseConfig, ServiceWorkerConfigs);
]
Pinterest 发现实施他们应用的外壳有些难。因为桌面时代(desktop-era)会假定多少数据能够通过有线连接发送出去,而其应用外壳的初始有效负载量很大包含有很多无关紧要的信息,比如用户的测试组,用户信息,上下文信息等。
他们不得不问自己:“我们是否应该把这些内容缓存在应用程序的外壳中?或者选择在渲染任何内容之前忍受阻塞网络请求对性能的影响。”
最终,他们选择这些内容缓存到应用外壳中,这就需要对什么时候应该让应用外壳失效(注销、从设置更新用户信息等)进行一定的管理。每一个请求的响应有一个‘appVersion’,如果应用程序的版本发生了变化,他们会先取消注册 Service Worker,转而注册新的请求,然后在下一次路由更改时重新加载整个页面。
Pinterest 用了 Lighthouse 对其性能的提升进行一次性的验证,以确保相关性能改进的方向是正确的。观察类似于持续互动时间这类的指标是很有用的。
下一年,他们希望用 Lighthouse 作为回归机制(regression mechanism)来验证页面的加载速度是否仍然快速。
Pinterest 刚刚部署了对 web 推送通知的支持,并且也在致力于提高未经身份验证(注销)时的用户体验。
他们有兴趣探索对于<link rel = preload>的支持,用其来预加载关键包和减少在首次加载时传送给用户的无用 JavaScript。请继续期待他们未来更好的用户体验!
在此祝贺 Pinterest 的 Zack Argyle、YenWei Liu、 Luna Ruan、Victoria Kwong、 Imad Elyafi、 Langtian Lang、Becky Stoneman 和 Ben Finkel 推出了他们的 Progressive Web App ,也感谢他们对于本文的贡献。也感谢 Jeffrey Posnick 和 Zouhir 对本文的审读。
原文来自:前端之巅
声明:所有来源为“聚合数据”的内容信息,未经本网许可,不得转载!如对内容有异议或投诉,请与我们联系。邮箱:marketing@think-land.com
通过企业关键词查询企业涉讼详情,如裁判文书、开庭公告、执行公告、失信公告、案件流程等等。
IP反查域名是通过IP查询相关联的域名信息的功能,它提供IP地址历史上绑定过的域名信息。
结合权威身份认证的精准人脸风险查询服务,提升人脸应用及身份认证生态的安全性。人脸风险情报库,覆盖范围广、准确性高,数据权威可靠。
全国城市和站点空气质量查询,污染物浓度及空气质量分指数、空气质量指数、首要污染物及空气质量级别、健康指引及建议采取的措施等。
输入手机号和拦截等级,查看是否是风险号码