- 原文地址: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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。