- 原文地址:React Performance Fixes on Airbnb Listing Pages
- 原文做者:Joe Lencioni
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:木羽 zwwill
- 校對者:tvChan, atuooo(史金煒)
簡要:可能在某些領域存在一些觸手可及的性能優化點,雖不常見但依然很重要。javascript
咱們一直在努力把 airbnb.com 的核心預訂流程遷移到一個使用 React Router 和 Hypernova 技術的服務端渲染的單頁應用。年初,咱們推出了登錄頁面,搜索結果告訴咱們很成功。咱們的下一步是將清單詳情頁擴展到單頁應用程序裏去。css
airbnb.com 的清單詳情頁: www.airbnb.com/rooms/8357前端
這是您在肯定預訂清單時所訪問的頁面。在整個搜索過程當中,您可能會屢次訪問該頁面以查看不一樣的清單。這是 airbnb 網站訪問量最大同時也是最重要的頁面之一,所以,咱們必須作好每個細節。java
做爲遷移到咱們的單頁應用的一部分,我但願能排查出全部影響清單頁交互性能的遺留問題(例如,滾動、點擊、輸入)。讓頁面啓動更快而且延遲更短,這符合咱們的目標,並且這會讓使用咱們網站的人們有更好的體驗。react
經過解析、修復、再解析的流程,咱們極大地提升了這個關鍵頁的交互性能,使得預訂體驗更加順暢,更使人滿意。在這篇文章中,您將瞭解到我用來解析這個頁面的技術,用來優化它的工具,以及在解析結果給出的火焰圖表中感覺優化的效果。android
這些配置項經過Chrome的性能工具被記錄下來:webpack
?react_perf
在查詢字符串中進行配置訪問本地開發頁面(啓用 React 的 User Timing 註釋,並禁用一些會使頁面變慢的 dev-only 功能,例如 axe-core)一般狀況下,我推薦在移動設備上進行解析以瞭解在較慢的設備上的用戶體驗,好比 Moto C Plus,或者 CPU 速度設置爲 6x 減速。然而,因爲這些問題已經足夠嚴重了,以致於即便是在沒有節流的狀況下,在個人高性能筆記本電腦上結果表現也是明顯得糟糕。ios
在我開始優化這個頁面時,我注意到控制檯上有一個警告:💀git
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 就會給出這樣的提醒 ✋ 。github
不過,錯誤信息並無明確地代表底發生了什麼,或者可能的緣由是什麼,但確實給了咱們一些線索。🔎 我注意到一些看起來像 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
這個文件,一個在頂部渲染的搜索組件。
事實上,咱們使用了一些特徵檢測,以確保在舊瀏覽器(如 IE)中能夠看到 placeholder
,若是在當前的瀏覽器中不支持 placeholder
,則會以不一樣的方式呈現 input
。特徵檢測是正確的方法(與用戶代理嗅探相反),可是因爲在服務器渲染時沒有瀏覽器檢測功能,致使服務器老是會渲染一些額外的內容,而不是大多數瀏覽器將呈現的內容。
這不只下降了性能,還致使了一些額外的標籤被渲染出來,而後每次再從頁面上刪除。真難伺候!我把渲染的內容轉化爲 React 的 state,並將其設置到 componentDidMount
,直到客戶端渲染時才呈現。這完美的解決了問題。
我從新運行了一遍 profiler 發現,<SummaryContainer>
在 mounting 後馬上更新。
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>
組件上。
BookIt 的重繪消耗了 103.15ms
有趣的是,除非用戶操做,這些組件基本是不可見的 👻 。
解決這個問題的方法是在不須要的時候不渲染這些組件。這加快了初始化的渲染,清除了一些沒必要要的重繪。🐎 若是咱們進一步地進行優化,增長更多 PureComponents,那麼初始化渲染會變得更快。
BookIt 的重繪消耗了 8.52ms
一般咱們會在清單頁面上作一些平滑滾動的效果,但在滾動時效果並不理想。📜 當動畫沒有達到平滑的 60 fps(每秒幀),甚至是 120 fps,人們一般會感到不舒服也不會滿意。滾動是一種特殊的動畫,是你的手指動做的直接反饋,因此它比其餘動畫更加敏感。
稍微分析一下後,我發現咱們在滾動事件處理機制中作了不少沒必要要的 React 組件的重繪!看起來真的很糟糕:
在沒作修復以前,Airbnb 上的滾動性能真的很糟糕
我可使用 React.PureComponent
轉化 <Amenity>
、<BookItPriceHeader>
和 <StickyNavigationController>
這三個組件來解決絕大部分問題。這大大下降了頁面重繪的成本。雖然咱們還沒能達到 60 fps(每秒幀數),但已經很接近了。
通過一些修改後,Airbnb 清單頁面的滾動性能略有改善
另外還有一些能夠優化的部分。展開火焰圖表,咱們能夠看到,<StickyNavigationController>
也產生了耗時的重繪。若是咱們細看他的組件堆棧信息,能夠發現四個類似的模塊。
StickyNavigationController 的重繪消耗了 8.52ms
<StickyNavigationController>
是清單頁面頂部的一個部分,當咱們不一樣部分間滾動時,它會聯動高亮您當前所在的位置。火焰圖表中的每一塊都對應着常駐導航的四個連接之一。而且,當咱們在兩個部分間滾動時,會高亮不一樣的連接,因此有些連接是須要重繪的,就像下圖顯示的那樣。
如今,我注意到咱們這裏有四個連接,在狀態切換時改變外觀的只有兩個,但在咱們的火焰圖表中顯示,四個連接每都作了重繪操做。這是由於咱們的 <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() {
...
}
}
複製代碼
在優化後的解析中咱們能夠看到,只有兩個連接被重繪,事半功倍!而且,若是咱們這裏有更多的連接塊,那麼渲染的工做量將再也不增長。
StickyNavigationController 的重繪消耗了 8.52ms
Dounan Shi 再 Flexport 一直在維護 Reflective Bind,這是供你用來作這類優化的 Babel 插件。這個項目還處於起步階段,還不足以正式發佈,但我已經對它將來的可能性感到興奮了。
繼續看 Performance 記錄的 Main 面板,我注意到咱們有一個很是可疑的模塊 handleScroll
,每次滾動事件都會消耗 19ms。若是咱們要達到 60 fps 就只有 16ms 的渲染時間,這明顯超出太多。
_handleScroll
消耗了 18.45ms
罪魁禍首的好像是 onLeaveWithTracking
內的某個部分。經過代碼排查,問題定位到了 <EngagementWrapper>
。而後在看看他的調用棧,發現大部分的時間消耗在了 React setState
,但奇怪的是,咱們並無發現期間有產生任何的重繪。
深刻挖掘 <EngagementWrapper>
,我注意到,咱們使用了 React 的 state 跟蹤了實例上的一些信息。
this.state = { inViewport: false };
複製代碼
然而,在渲染的流程中咱們歷來沒有使用過這個 state,也沒有監聽它的變化來作重繪,也就是說,咱們作了無用功。將全部 React 的此類 state 用法轉換爲簡單的實例變量可讓這些滾動動畫更流暢。
this.inViewport = false;
複製代碼
滾動事件的 handler 消耗了 1.16ms
我還注意到,<AboutThisListingContainer>
的重繪致使了組件 <Amenities>
高消耗且多餘的重繪。
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 清單頁的優化滾動表現
更多地體驗過這個頁面後,我明顯得感受到在點擊「Helpful」按鈕時存在延時問題。
個人直覺告訴我,點擊這個按鈕致使頁面上的全部評論都被從新渲染了。看一看火焰圖表,和我預計的同樣:
ReviewsContent 重繪消耗了 42.38ms
在這兩個地方引入 React.PureComponent
以後,咱們讓頁面的更新更高效。
ReviewsContent 重繪消耗了 12.38ms
再回到以前的客戶端/服務端不匹配的老問題上,我注意到,在這個輸入框裏打字確實有反應遲鈍的感受。
分析後發現,每次按鍵操做都會形成整個評論區頭部的重繪。這是在逗我嗎?😱
Redux-connected ReviewsContainer 重繪消耗 61.32ms
爲了解決這個問題,我把頭部的一部分提取出來作爲組件,以便我能夠把它作成一個 React.PureComponent
,而後再把這個幾個 React.PureComponent
分散在構建樹上。這使得每次按鍵操做就只能重繪須要重繪的組件了,也就是 input
。
ReviewsHeader 重繪消耗 3.18ms
React.PureComponent
和 reselect
在咱們 React 應用的性能優化工具中是很是有用的兩個工具。若是你喜歡作性能優化,那就加入咱們吧,咱們正在尋找才華橫溢、對一切都很好奇的你。咱們知道,Airbnb 還有大優化的空間,若是你發現了一些咱們可能感興趣的事,亦或者只是想和我聊聊天,你能夠在 Twitter 上找到我 @lencioni。
着重感謝 Thai Nguyen 在 review 代碼和清單頁遷移到單頁應用的過程當中做出的貢獻。♨️ 得以實施主要得感謝 Chrome DevTools 團隊,這些性能可視化的工具實在是太棒了!另外 Netflix 是第二項優化的功臣。
感謝 Adam Neary。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。