Pinterest PWA性能的案例研究

HerB28 6年前發布 | 16K 次閱讀 PWA 移動開發

Pinterest PWA性能的案例研究

可在手機上登錄 https://pinterest.com 去體驗下Pinterest新的移動端網站

為什么Pinterest會選擇用PWA?簡單回顧下相關的歷史

在最開始的時候,因為專注于國際市場的增長,Pinterest關注了移動端網頁的開發,也由此有了Pinterest PWA。

在分析了未經驗證的移動端網頁用戶的相關數據后,Pinterest發現他們原來舊而慢的網絡體驗僅能將1%的用戶轉化為注冊、登錄或下載app作為本地應用使用的用戶。如果能夠提升這一轉化率的話,無疑是一個巨大的機會,所以他們開始了對PWA的投資。

在一個季度內建立和推出PWA

用時超過 3個月 ,Pinterest通過使用React、Redux和webpack重構了他們移動端網頁的體驗。移動端網頁的重寫也提高了他們幾項核心業務指標。

與舊移動端網頁的體驗相比,新移動端網頁用戶的使用時間增加了 40% ,用戶生成的廣告收益增加了 44% ,并且核心業務增長了 60%

Pinterest PWA性能的案例研究

與此同時,移動端網頁的重寫也改善了Pinterest網頁的一些性能。

Pinterest PWA在3G普通移動硬件上的加載速度很快

Pinterest舊的移動端網頁含有大量的需要占用很多CPU的JavaScript包,延長了Pin網頁加載和取得互動 所需的時間

在可以進行任何互動之前,用戶經常需要等 23秒

Pinterest PWA性能的案例研究

Pinterest原有的移動端網站需要花費23s取得互動。這一過程中,他們會發送2.5MB以上的JavaScript,其中約有1.5MB用于主包,1MB用于懶加載。在主線程最終能夠實現交互之前,需要花費幾秒鐘的時間來解析和編譯

他們新移動端網頁的體驗有了極大的提高。

不僅是因為他們分散和減少了數百KB的JavaScript,將核心包體的大小從650KB降到了150KB,也是因為他們提高了網頁的一些關鍵性能指標。 首次有效繪制 時間由4.2s降低到了1.8s,并且 可交互時間 由23s降低到了5.6s。

Pinterest PWA性能的案例研究

以上的測試結果是在連接了緩慢3G網絡的普通Android硬件上得到的。在重復訪問的情況下,結果甚至更好。

得益于 服務工作線程緩存了主要的JavaScript、CSS和靜態UI資源,重復訪問的時間被縮短到了3.9s:

Pinterest PWA性能的案例研究

盡管Pinterest有iOS和Android應用,但是只需在開始時下載約為150KB優化壓縮(minified & gzipped)過的代碼,就能夠在網頁應用上實現與本地應用相同的主頁推送體驗。對比于Android版應用的9.6MB和iOS版應用的56MB:

Pinterest PWA性能的案例研究

然而值得注意的是,與本地應用相比Pinterest PWA的優點并不局限于前期主頁推送體驗。PWA還會按新路由的需要來加載代碼,而且額外代碼的成本會被分攤到使用網頁應用的整個過程中。隨后的導航仍然不會像下載應用那樣消耗大量的數據。

Pinterest PWA性能的案例研究

Pinterest的PWA分別在移動端的Firefox、Edge和Safari上的顯示

基于路由的JavaScript分塊(chunking)

在前期 僅加載用戶需要的代碼 降低了 網絡傳輸和解析/編譯JavaScript 的時間,從而提高了網頁的加載速度和縮短了實現交互的時間。隨后非關鍵資源可以根據需要進行懶加載。

Pinterest開始將原有的高達幾個MB的JavaScript包拆分成3種不同類型的webpack模塊,效果還挺不錯:

Pinterest PWA性能的案例研究

  • 一類是包含外部依賴性的 vendor 模塊(react、redux、react-router等),大約73KB
  • 一類是包含渲染應用所需要的大部分代碼的 入口 模塊(entry chunk)(即常見的庫,主要的頁面外殼,我們的redux store),大約72KB
  • 一類是包含關于單個路由的代碼的 異步 路由模塊(async route chunk),大約13到18KB

以下Network的瀑布記錄,突出顯示了漸進式地按需傳送代碼如何避免了整體(monolithic)傳送包體的需求:

Pinterest PWA性能的案例研究

( 對于長期緩存,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);
    }
  })
];

( 原代碼見 sample-webpack.js hosted with ? by GitHub )

在分塊的過程中,他們也用了 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 });
    });
  }
}

原代碼見 sample-codesplitting.js hosted with ? by GitHub

用babel-preset-env來只編譯(transpile)目標瀏覽器所需的內容

Pinterest用了Babel的 babel-preset-env 來僅編譯(transpile)不受目標瀏覽器支持的ES2015+功能。Pinterest針對的是現代瀏覽器最新的兩個版本,他們的.babelrc設置類似于:

{
  "presets": [
    ["env", {
      "targets": {
        "browsers": ["last 2 versions"]
      }
    }]
  ]
}

( 原代碼見: .babelrc hosted with ? by GitHub )

其實Pinterest也可以對此作進一步的優化,按照實際需要有條件地提供polyfills(比如:Safari 國際化的API )。但是目前這還是這一優化仍在計劃中。

使用Webpack Bundle Analyzer來分析改進空間

Webpack Bundle Analyzer 是一個很好的工具,可以幫助人切實地理解傳送給客戶的JavaScript包之間的依賴關系。

如下圖所示,在早期的Pinterest版本的輸出中,有很多的紫色,粉色和藍色的區域。這些都是被懶加載的路由 異步 模塊。Webpack Bundle Analyzer可以幫助Pinterest將大多數的含有 重復代碼 的模塊可視化:

Pinterest PWA性能的案例研究

Webpack Bundle Analyzer可以將重復代碼在不同模塊之間的大小比例視覺化。

在有了所有模塊中有重復代碼的信息之后,Pinterest就可以做出調用。 他們把異步模塊中的重復代碼移到了主要模塊中。雖然這一改動增加了20%入口模塊的大小,但是卻將所有懶加載模塊的大小減小了90%!

Pinterest PWA性能的案例研究

圖像優化

大部分Pinterest PWA中內容的懶加載都是通過無限網格瀑布流插件 Masonry 來處理的。它內置了對虛擬化的支持,并且僅裝載(mounting)視口內的子項。

Pinterest PWA性能的案例研究

Pinterest也在他們的PWA中使用了漸進式加載圖片的技術。有主導顏色的占位符在最開始會被用于每一個Pin。而Pin的圖像會以 Progressive JPEGs 來提供,其質量會隨著掃描次數的增加而增加:

Pinterest PWA性能的案例研究

React性能的痛點

在Pinterest使用網格瀑布流 Masonry 插件的同時,他們也面臨著React帶來的一些渲染性能的問題。裝載和卸載大的組件樹(像Pin)可能會很慢。一個Pin里面有很多的東西:

Pinterest PWA性能的案例研究

盡管當時他們寫Pinterest的時候用的是React 15.5.4, 但是他們寄希望于 React 16 (Fiber)將會大大減少卸載所用的時間。與此同時, 虛擬化的網格 也會顯著地減少組件卸載的時間。

Pinterest還會限制Pin的插入,以便更快地測量/渲染第一個Pin,但是這也意味著設備CPU的工作量更大了。

導航轉換

為了提高感知性能,Pinterest也更新了導航欄圖標的選定狀態,將其獨立于路由之外。這就確保了當導航從一個路由轉到另一個路由的時候,用戶并不會因為網絡的阻塞而感到緩慢。用戶在等待數據到達時可以快速地獲得可視化界面。

Pinterest PWA性能的案例研究

使用Redux的體驗

Pinterest在他們所有的API數據中均使用了 normalizr (normalizr會根據一種模式來規范化嵌套的JSON)。從Redux DevTools就可以看出:

Pinterest PWA性能的案例研究

這樣做的缺點是逆規范化(denormalization)會變得很慢,在渲染的階段最終他們很大程度上是依賴于 reselect 的selector模式來記憶(memoizing)逆規范化。他們也盡可能的在最低程度上進行逆規范處理,以確保單個的更新不會導致大規模的重新渲染。

舉個例子來說,他們的網格項目列表只是由Pin ID與逆規范化自身的Pin組件組成的。如果任何給定的Pin有了改變,則完整的網格不必重新渲染。但是有得就有失,這樣Pinterest PWA就有了很多Redux用戶,雖然這一點尚未對性能產生顯著的影響。

用Service Worker來緩存資源

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
  ],
});

( 原代碼見: sample-sw-caching.js hosted with ? by GitHub)

如今,Pinterest使用緩存優先策略(cache-first strategy)來緩存任何JavaScript或者CSS的包,并且也會緩存其用戶的界面(應用程序的外殼)。

Pinterest PWA性能的案例研究

在緩存資源優先的設置中,如果請求與緩存條目相匹配,則以緩存的資源為準。否則,則嘗試從網絡獲取資源。如果網絡請求成功,則對緩存進行更新。要了解更多有關使用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 PWA性能的案例研究

  • 在這之后,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);
]

原代碼見: sample-sw-generation.js hosted with ? by GitHub

應用外殼的挑戰

Pinterest發現實施他們應用的外殼有些難。因為桌面時代(desktop-era)會假定多少數據能夠通過有線連接發送出去,而其應用外殼的初始有效負載量很大包含有很多無關緊要的信息,比如用戶的測試組,用戶信息,上下文信息等。

他們不得不問自己:“我們是否應該把這些內容緩存在應用程序的外殼中?或者選擇在渲染任何內容之前忍受阻塞網絡請求對性能的影響。”

Pinterest PWA性能的案例研究

最終,他們選擇這些內容緩存到應用外殼中,這就需要對什么時候應該讓應用外殼失效(注銷、從設置更新用戶信息等)進行一定的管理。每一個請求的響應有一個‘appVersion’,如果應用程序的版本發生了變化,他們會先取消注冊Service Worker,轉而注冊新的請求,然后在下一次路由更改時重新加載整個頁面。

用Lighthouse進行審查

Pinterest用了 Lighthouse 對其性能的提升進行一次性的驗證,以確保相關性能改進的方向是正確的。觀察類似于持續互動時間這類的指標是很有用的。

Pinterest PWA性能的案例研究

下一年,他們希望用Lighthouse作為回歸機制(regression mechanism)來驗證頁面的加載速度是否仍然快速。

未來

Pinterest剛剛部署了對web推送通知的支持,并且也在致力于提高未經身份驗證(注銷)時的用戶體驗。

Pinterest PWA性能的案例研究

他們有興趣探索對于< link rel = preload >的支持,用其來預加載關鍵包和減少在首次加載時傳送給用戶的無用JavaScript。請繼續期待他們未來更好的用戶體驗!

 

 

來自:http://www.infoq.com/cn/articles/pinterest-progressive-web-app-performance-case-study

 

 本文由用戶 HerB28 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!