【譯】針對 Airbnb 清單頁的 React 效能優化

木羽v發表於2017-12-21

針對 Airbnb 清單頁的 React 效能優化

簡要:可能在某些領域存在一些觸手可及的效能優化點,雖不常見但依然很重要。


我們一直在努力把 airbnb.com 的核心預訂流程遷移到一個使用 React RouterHypernova 技術的服務端渲染的單頁應用。年初,我們推出了登陸頁面,搜尋結果告訴我們很成功。我們的下一步是將清單詳情頁擴充套件到單頁應用程式裡去。

【譯】針對 Airbnb 清單頁的 React 效能優化

airbnb.com 的清單詳情頁: www.airbnb.com/rooms/8357

這是您在確定預訂清單時所訪問的頁面。在整個搜尋過程中,您可能會多次訪問該頁面以檢視不同的清單。這是 airbnb 網站訪問量最大同時也是最重要的頁面之一,因此,我們必須做好每一個細節。

作為遷移到我們的單頁應用的一部分,我希望能排查出所有影響清單頁互動效能的遺留問題(例如,滾動、點選、輸入)。讓頁面啟動更快並且延遲更短,這符合我們的目標,而且這會讓使用我們網站的人們有更好的體驗。

通過解析、修復、再解析的流程,我們極大地提高了這個關鍵頁的互動效能,使得預訂體驗更加順暢,更令人滿意。在這篇文章中,您將瞭解到我用來解析這個頁面的技術,用來優化它的工具,以及在解析結果給出的火焰圖表中感受優化的效果。

方法

這些配置項通過Chrome的效能工具被記錄下來:

  1. 開啟隱身視窗(這樣我的瀏覽器擴充套件工具不會干擾我的解析)。
  2. 使用 ?react_perf 在查詢字串中進行配置訪問本地開發頁面(啟用 React 的 User Timing 註釋,並禁用一些會使頁面變慢的 dev-only 功能,例如 axe-core
  3. 點選 record 按鈕 ⚫️
  4. 操作頁面(如:滾動,點選,打字)
  5. 再次點選 record 按鈕 ?,分析結果

【譯】針對 Airbnb 清單頁的 React 效能優化

通常情況下,我推薦在移動裝置上進行解析以瞭解在較慢的裝置上的使用者體驗,比如 Moto C Plus,或者 CPU 速度設定為 6x 減速。然而,由於這些問題已經足夠嚴重了,以至於即使是在沒有節流的情況下,在我的高效能膝上型電腦上結果表現也是明顯得糟糕。

初始化渲染

在我開始優化這個頁面時,我注意到控制檯上有一個警告:?

webpack-internal:///36:36 Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server: (client) ut-placeholder-label screen-reader-only" (server) ut-placeholder-label" data-reactid="628"
複製程式碼

這是可怕的 客戶端/服務端 不匹配問題,當伺服器渲染不同於客戶端初始化渲染時發生。這會迫使你的 Web 瀏覽器執行那些在使用伺服器渲染時不應該做的工作,所以每當發生這種情況時 React 就會給出這樣的提醒 ✋ 。

不過,錯誤資訊並沒有明確地表明底發生了什麼,或者可能的原因是什麼,但確實給了我們一些線索。? 我注意到一些看起來像 CSS 類的文字,所以我在終端裡輸入下面的命令:

~/airbnb ❯❯❯ ag ut-placeholder-label
app/assets/javascripts/components/o2/PlaceholderLabel.jsx
85:        'input-placeholder-label': true,

app/assets/stylesheets/p1/search/_SearchForm.scss
77:    .input-placeholder-label {
321:.input-placeholder-label,

spec/javascripts/components/o2/PlaceholderLabel_spec.jsx
25:    const placeholderContainer = wrapper.find('.input-placeholder-label');
複製程式碼

很快地我將搜尋範圍縮小到了 o2/PlaceHolderLabel.jsx 這個檔案,一個在頂部渲染的搜尋元件。

【譯】針對 Airbnb 清單頁的 React 效能優化

事實上,我們使用了一些特徵檢測,以確保在舊瀏覽器(如 IE)中可以看到 placeholder,如果在當前的瀏覽器中不支援 placeholder,則會以不同的方式呈現 input。特徵檢測是正確的方法(與使用者代理嗅探相反),但是由於在伺服器渲染時沒有瀏覽器檢測功能,導致伺服器總是會渲染一些額外的內容,而不是大多數瀏覽器將呈現的內容。

這不僅降低了效能,還導致了一些額外的標籤被渲染出來,然後每次再從頁面上刪除。真難伺候!我把渲染的內容轉化為 React 的 state,並將其設定到 componentDidMount,直到客戶端渲染時才呈現。這完美的解決了問題。

【譯】針對 Airbnb 清單頁的 React 效能優化

我重新執行了一遍 profiler 發現,<SummaryContainer> 在 mounting 後立刻更新。

【譯】針對 Airbnb 清單頁的 React 效能優化

Redux 連線的 SummaryContainer 重繪消耗了 101.64 ms

更新後會重新渲染一個 <BreadcrumbList>、兩個 <ListingTitles> 和一個 <SummaryIconRow> 元件,但是他們前後並沒有任何區別,所以我們可以通過使用 React.PureComponent 使這三個元件的渲染得到顯著的優化。方法很簡單,如下

export default class SummaryIconRow extends React.Component {
  ...
}
複製程式碼

改成這樣:

export default class SummaryIconRow extends React.PureComponent {
  ...
}
複製程式碼

接下來,我們可以看到 <BookIt> 在頁面初始載入時也發生了重新渲染的操作。根據火焰圖可以看出,大部分時間都消耗在渲染 <GuestPickerTrigger><GuestCountFilter> 元件上。

【譯】針對 Airbnb 清單頁的 React 效能優化

BookIt 的重繪消耗了 103.15ms

有趣的是,除非使用者操作,這些元件基本是不可見的 ? 。

【譯】針對 Airbnb 清單頁的 React 效能優化

解決這個問題的方法是在不需要的時候不渲染這些元件。這加快了初始化的渲染,清除了一些不必要的重繪。? 如果我們進一步地進行優化,增加更多 PureComponents,那麼初始化渲染會變得更快。

【譯】針對 Airbnb 清單頁的 React 效能優化

BookIt 的重繪消耗了 8.52ms

來回滾動

通常我們會在清單頁面上做一些平滑滾動的效果,但在滾動時效果並不理想。? 當動畫沒有達到平滑的 60 fps(每秒幀),甚至是 120 fps,人們通常會感到不舒服也不會滿意。滾動是一種特殊的動畫,是你的手指動作的直接反饋,所以它比其他動畫更加敏感

稍微分析一下後,我發現我們在滾動事件處理機制中做了很多不必要的 React 元件的重繪!看起來真的很糟糕:

【譯】針對 Airbnb 清單頁的 React 效能優化

在沒做修復之前,Airbnb 上的滾動效能真的很糟糕

我可以使用 React.PureComponent 轉化 <Amenity><BookItPriceHeader><StickyNavigationController> 這三個元件來解決絕大部分問題。這大大降低了頁面重繪的成本。雖然我們還沒能達到 60 fps(每秒幀數),但已經很接近了。

【譯】針對 Airbnb 清單頁的 React 效能優化

經過一些修改後,Airbnb 清單頁面的滾動效能略有改善

另外還有一些可以優化的部分。展開火焰圖表,我們可以看到,<StickyNavigationController> 也產生了耗時的重繪。如果我們細看他的元件堆疊資訊,可以發現四個相似的模組。

【譯】針對 Airbnb 清單頁的 React 效能優化

StickyNavigationController 的重繪消耗了 8.52ms

<StickyNavigationController> 是清單頁面頂部的一個部分,當我們不同部分間滾動時,它會聯動高亮您當前所在的位置。火焰圖表中的每一塊都對應著常駐導航的四個連結之一。並且,當我們在兩個部分間滾動時,會高亮不同的連結,所以有些連結是需要重繪的,就像下圖顯示的那樣。

【譯】針對 Airbnb 清單頁的 React 效能優化

現在,我注意到我們這裡有四個連結,在狀態切換時改變外觀的只有兩個,但在我們的火焰圖表中顯示,四個連結每都做了重繪操作。這是因為我們的 <NavigationAnchors> 元件每次切換渲染時都建立一個新的方法作為引數傳遞給 <NavigationAnchor>,這違背了我們純元件的優化原則。

const anchors = React.Children.map(children, (child, index) => {      
  return React.cloneElement(child, {
    selected: activeAnchorIndex === index,
    onPress(event) { onAnchorPress(index, event); },
  });
});
複製程式碼

我們可以通過確保 <NavigationAnchor> 每次被 <NavigationAnchors> 渲染時接收到的都是同一個 function 來解決這個問題。

const anchors = React.Children.map(children, (child, index) => {      
  return React.cloneElement(child, {
    selected: activeAnchorIndex === index,
    index,
    onPress: this.handlePress,
  });
});
複製程式碼

接下來是 <NavigationAnchor>

class NavigationAnchor extends React.Component {
  constructor(props) {
    super(props);
    this.handlePress = this.handlePress.bind(this);
  }

 handlePress(event) {
    this.props.onPress(this.props.index, event);
  }

  render() {
    ...
  }
}
複製程式碼

在優化後的解析中我們可以看到,只有兩個連結被重繪,事半功倍!並且,如果我們這裡有更多的連結塊,那麼渲染的工作量將不再增加。

【譯】針對 Airbnb 清單頁的 React 效能優化

StickyNavigationController 的重繪消耗了 8.52ms

Dounan ShiFlexport 一直在維護 Reflective Bind,這是供你用來做這類優化的 Babel 外掛。這個專案還處於起步階段,還不足以正式釋出,但我已經對它未來的可能性感到興奮了。

繼續看 Performance 記錄的 Main 皮膚,我注意到我們有一個非常可疑的模組 handleScroll,每次滾動事件都會消耗 19ms。如果我們要達到 60 fps 就只有 16ms 的渲染時間,這明顯超出太多。

【譯】針對 Airbnb 清單頁的 React 效能優化

_handleScroll 消耗了 18.45ms

罪魁禍首的好像是 onLeaveWithTracking 內的某個部分。通過程式碼排查,問題定位到了 <EngagementWrapper>。然後在看看他的呼叫棧,發現大部分的時間消耗在了 React setState,但奇怪的是,我們並沒有發現期間有產生任何的重繪。

深入挖掘 <EngagementWrapper>,我注意到,我們使用了 React 的 state 跟蹤了例項上的一些資訊。

this.state = { inViewport: false };
複製程式碼

然而,在渲染的流程中我們從來沒有使用過這個 state,也沒有監聽它的變化來做重繪,也就是說,我們做了無用功。將所有 React 的此類 state 用法轉換為簡單的例項變數可以讓這些滾動動畫更流暢。

this.inViewport = false;
複製程式碼

【譯】針對 Airbnb 清單頁的 React 效能優化

滾動事件的 handler 消耗了 1.16ms

我還注意到,<AboutThisListingContainer> 的重繪導致了元件 <Amenities> 高消耗且多餘的重繪。

【譯】針對 Airbnb 清單頁的 React 效能優化

AboutThisListingContainer 的重繪消耗了 32.24ms

最終確認是我們使用的高階元件 withExperiments 來幫助我們進行實驗所造成的。HOC 每次都會建立一個新的物件作為引數傳遞給子元件,整個流程都沒有做任何優化。

render() {
  ...
  const finalExperiments = {
    ...experiments,
    ...this.state.experiments,
  };
  return (
    <WrappedComponent
      {...otherProps}
      experiments={finalExperiments}
    />
  );
}
複製程式碼

我通過引入 reselect 來修復這個問題,他可以快取上一次的結果以便在連續的渲染中保持相同的引用。

const getExperiments = createSelector(
  ({ experimentsFromProps }) => experimentsFromProps,
  ({ experimentsFromState }) => experimentsFromState,
  (experimentsFromProps, experimentsFromState) => ({
    ...experimentsFromProps,
    ...experimentsFromState,
  }),
);
...
render() {
  ...
  const finalExperiments = getExperiments({
    experimentsFromProps: experiments,
    experimentsFromState: this.state.experiments,
  });
  return (
    <WrappedComponent
      {...otherProps}
      experiments={finalExperiments}
    />
  );
}
複製程式碼

問題的第二個部分也是相似的。我們使用了 getFilteredAmenities 方法將一個陣列作為第一個引數,並返回該陣列的過濾版本,類似於:

function getFilteredAmenities(amenities) {
  return amenities.filter(shouldDisplayAmenity);
}
複製程式碼

雖然看上去沒什麼問題,但是每次執行即使結果相同也會建立一個新的陣列例項,這使得即使是很單純的元件也會重複的接收這個陣列。我同樣是通過引入 reselect 快取這個過濾器來解決這個問題。?

可能還有更多的優化空間,(比如 CSS containment),不過現在看起來已經很好了。

【譯】針對 Airbnb 清單頁的 React 效能優化

修復後的 Airbnb 清單頁的優化滾動表現

點選操作

更多地體驗過這個頁面後,我明顯得感覺到在點選「Helpful」按鈕時存在延時問題。

【譯】針對 Airbnb 清單頁的 React 效能優化

我的直覺告訴我,點選這個按鈕導致頁面上的所有評論都被重新渲染了。看一看火焰圖表,和我預計的一樣:

【譯】針對 Airbnb 清單頁的 React 效能優化

ReviewsContent 重繪消耗了 42.38ms

在這兩個地方引入 React.PureComponent 之後,我們讓頁面的更新更高效。

【譯】針對 Airbnb 清單頁的 React 效能優化

ReviewsContent 重繪消耗了 12.38ms

鍵盤操作

再回到之前的客戶端/服務端不匹配的老問題上,我注意到,在這個輸入框裡打字確實有反應遲鈍的感覺。

【譯】針對 Airbnb 清單頁的 React 效能優化

分析後發現,每次按鍵操作都會造成整個評論區頭部的重繪。這是在逗我嗎??

【譯】針對 Airbnb 清單頁的 React 效能優化

Redux-connected ReviewsContainer 重繪消耗 61.32ms

為了解決這個問題,我把頭部的一部分提取出來做為元件,以便我可以把它做成一個 React.PureComponent,然後再把這個幾個 React.PureComponent 分散在構建樹上。這使得每次按鍵操作就只能重繪需要重繪的元件了,也就是 input

【譯】針對 Airbnb 清單頁的 React 效能優化

ReviewsHeader 重繪消耗 3.18ms

我們學到了什麼?

  • 我們希望頁面可以啟動得更快延遲更短
  • 這意味著我們需要關注不僅僅是頁面互動時間,還需要對頁面上的互動進行剖析,比如滾動、點選和鍵盤事件。
  • React.PureComponentreselect 在我們 React 應用的效能優化工具中是非常有用的兩個工具。
  • 當例項變數這種輕量級的工具可以完美地滿足你的需求時,就不要使用像 React state 這種重量級的工具了。
  • 雖然 React 很強大,但有時編寫程式碼來優化你的應用反而更容易。
  • 培養分析、優化、再分析的習慣。

如果你喜歡做效能優化那就加入我們吧我們正在尋找才華橫溢、對一切都很好奇的你。我們知道,Airbnb 還有大優化的空間,如果你發現了一些我們可能感興趣的事,亦或者只是想和我聊聊天,你可以在 Twitter 上找到我 @lencioni


著重感謝 Thai Nguyen 在 review 程式碼和清單頁遷移到單頁應用的過程中作出的貢獻。♨️ 得以實施主要得感謝 Chrome DevTools 團隊,這些效能視覺化的工具實在是太棒了!另外 Netflix 是第二項優化的功臣。

感謝 Adam Neary


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章