Airbnb 爱彼迎工程师和数据科学家将定期和大家分享移动开发、系统架构、数据科学及人工智能等领域的技术探索和经验心得。

正文从这开始~~

在一些容易被忽视但又非常重要的场景,可能会有许多严重影响性能却很容易解决的问题。

主要介绍了 Airbnb web 端访问量最大的页面之一——房源详情页的 react 性能优化过程,其中用到的方法、工具和经验心得。

我们使用 React Router 和 Hypernova 开发支持服务端渲染的单页面应用。第一个应用场景是 airbnb.com 的核心预订流程。在今年年初(注:本文原作于 2017 年),我们完成了首页和房源搜索结果页面的迁移并取得了很好的效果。下一步计划是把房源详情页面加入到单页面应用中。

airbnb.com房源详情页面: https://www.airbnb.com/rooms/8357

这就是我们的房源详情页面。在整个搜索过程中,用户可能会多次访问该页面,查看不同的房源。这个页面是 airbnb.com 上访问量最大,最重要的页面之一,因此我们希望弄清楚所有影响性能的细节!

每个页面都不可避免地会有一些交互操作,如滚动、点击、输入。作为单页面应用迁移工作的一部分,我希望排查房源详情页面中所有由交互操作引起的性能问题。我们希望页面可以快速启动并保持流畅,让用户拥有更好的体验。

通过分析,修复和再次分析的过程,这个关键页面的交互性能得到了显著改善,可以给用户带来更加流畅的预定体验。在这篇文章中,您将了解到我用来分析这个页面的技术以及优化页面的工具,并能从火焰图中看到改变带来的影响。

方法

对页面的分析是通过Chrome的性能工具记录的:

  • 打开隐身窗口(这样我的浏览器插件不会干扰到分析)

  • 在本地开发环境中访问要分析的页面,并在查询字段中使用 ?react_perf(以启用 React 的User Timing annotations),同时禁用一些会减慢页面速度的 dev-only 功能,比如 axe-core)

  • 单击录制按钮⚫️

  • 与页面交互(例如滚动,单击,输入)

  • 再次单击录制按钮?并分析结果

通常情况下,我提倡在移动硬件上进行性能分析,例如 Moto C Plus,或者将 CPU 限制设置为6x减速,以了解较慢设备上的体验。然而,因为这个页面的性能问题已经足够严重,所以在我配置很高的笔记本电脑上,即使不设置 throttle,也可以明显观察到性能问题的存在。

初始渲染

当我开始优化这个页面时,我注意到控制台有一个警告:?

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,它是位于评论区顶部用于搜索的组件。?

从代码中我发现,我们检测了一些浏览器功能来确保 Placeholder 在老版本浏览器(如 Internet Explorer)中可见,但如果当前浏览器中不支持,则会以不同方式渲染输入框。由于在服务端渲染阶段并不能对浏览器执行检测,导致服务端总会渲染一些额外的内容。

这不仅影响了性能,还会导致每次都有多余的标签被渲染出来。为了解决这个问题,我使用 react state 来渲染这部分内容,并把它放在 componentDidMount 中,直到客户端渲染时才会执行。 ?

再次运行 profiler,可以看到 <SummaryContainer> 在 initial mount 后立刻更新。

重新渲染 Redux 连接的 SummaryContainer,耗时 101.63 毫秒

在更新时,最终会重新渲染一个 <BreadcrumbList>,两个 <ListingTitles> 和一个 <SummaryIconRow>。但是,它们都没有任何变化,因此我们可以对这三个组件使用React.PureComponent 来大幅减少不必要的渲染操作。

export default class SummaryIconRow extends React.Component {...
}

改为:

export default class SummaryIconRow extends React.PureComponent {...
}

接下来,我们可以看到 <BookIt> 在页面加载时也发生了重新渲染。根据火焰?图,大部分时间是用在了渲染 <GuestPickerTrigger> 和 <GuestCountFilter> 上。

重新渲染 BookIt,耗时 103.15 毫秒

有趣的是,除非客户需要输入,否则这些组件是不可见的?。

解决此问题的方法是直到组件被使用时才进行渲染。这提高了初始渲染和重新渲染的速度。 ?如果我们再深入一点,使用更多的 PureComponents,可以让渲染速度更快。

重新渲染 BookIt,耗时 8.52 毫秒

上下滚动

在做一些优化平滑滚动动画的工作时,我注意到页面在滚动时非常不稳定。 ?当动画没有达到 60 fps(每秒帧数),甚至没有达到 120 fps 时,用户就能感觉到卡顿了。滚动是一种特殊的动画,它直接关联到手指的运动,所以在性能比较差时会比其他动画更敏感。

稍微分析一下,我发现我们在滚动事件处理机制(scroll event handlers)中对React组件进行了大量不必要的重新渲染!这真的非常糟糕:

在没做任何修复之前,Airbnb 房源详情页的滚动性能确实很差

将这些树中的三个组件(<Amenity>,<BookItPriceHeader>和<StickyNavigationController>)改成 React.PureComponent,能解决大部分问题,大大降低了重新渲染的开销。虽然我们还没有达到 60 fps,但我们更加接近了:

经过一些修复后,Airbnb 房源详情页的滚动性能略有提升

此外,还有一些可以优化的部分。?稍微展开一下火焰图,我们可以看到我们仍然花费了大量时间重新渲染 <StickyNavigationController>。而且,如果我们仔细看组件堆栈信息,可以发现有四个相似的模块:

重新渲染 StickyNavigationController,耗时 58.80 毫秒

<StickyNavigationController> 是房源页面的一部分,固定在顶部。当你在各模块之间滚动时,它会突出显示你当前所在的模块。火焰图中的四个块分别对应于吸顶导航的四个链接。当我们在各个模块之间滚动时,会突出显示不同的链接,因此其中一些需要重新渲染。这是它在浏览器中的效果。

我们可以看到一共有四个链接,但在各个部分之间切换时只有两个需要更新外观。在火焰图中,我们发现每次四个链接都会重新渲染。发生这种情况的原因是 <NavigationAnchors> 组件每次渲染时都创建了一个新函数,并将它作为 prop 传递给 <NavigationAnchor>,这会使 pure 组件失去优化的能力。

const anchors = React.Children.map(children, (child, index) =&gt; {     return React.cloneElement(child, {selected: activeAnchorIndex === index,onPress(event) { onAnchorPress(index, event); },});
});

我们可以通过确保 <NavigationAnchor> 每次被 <NavigationAnchors> 渲染时总是接收到相同的函数来修复这个问题:

const anchors = React.Children.map(children, (child, index) =&gt; {     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() {...}
}

优化后再运行 profiler,可以看到只重新渲染了两个链接,工作量变成了之前的一半?!而且,如果我们使用更多的链接,需要渲染的工作量也不会增加。

重新渲染 StickyNavigationController,耗时 32.85 毫秒

Flexport 的 Dounan Shi 一直在研究 Reflective Bind,它使用 Babel 插件来执行这一类的优化。这个项目还处于起步阶段,尚不足以正式发布,但我非常期待它的未来。

观察性能工具的主面板,我注意到我们有一个非常可疑的 _handleScroll 块,每次滚动事件都会占用 19ms。如果我们想要达到 60 fps,就只能有 16ms 的渲染时间,这明显超出太多了。 ?

_handleScroll 耗时18.45毫秒

罪魁祸首似乎在 onLeaveWithTracking 内部。通过搜索代码,我将其跟踪到<EngagementWrapper>。再仔细看看调用堆栈,我注意到大部分时间都花在了 React 的 setState 中,但奇怪的是我们实际上并没有看到任何重新渲染。嗯…

深入研究一下 <EngagementWrapper>,我注意到我们正在使用 React state 来跟踪实例上的一些信息。

this.state = { inViewport: false };

但是,我们从未在渲染路径(render path)中使用 inViewport,并且永远不需要在 inViewport 改变时触发重新渲染,也就是说我们付出了不必要的性能开销。 ?将 React state 的所有类似用法转换为简单的实例变量,有助于加快滚动动画的速度。

this.inViewport = false;

滚动事件处理程序耗时 1.16 毫秒

我还注意到 <AboutThisListingContainer> 的重新渲染导致了 <Amenities> 组件昂贵的?、不必要的重新渲染。

在 AboutThisListingContainer 中重新渲染耗时 32.24 毫秒

最终确认重新渲染是由用于帮助我们进行实验的 withExperiments 高阶组件引起的。这个 HOC 总是将新创建的对象作为 prop 传递给它包装的组件——失去了对其路径中的任何优化。

render() {...const finalExperiments = {...experiments,...this.state.experiments,};return (&lt;WrappedComponent{...otherProps}experiments={finalExperiments}/&gt;);
}

我通过引入 reselect 修复了这个问题,它会缓存上次的结果,在连续渲染之间可以保持引用相等性。

const getExperiments = createSelector(({ experimentsFromProps }) =&gt; experimentsFromProps,({ experimentsFromState }) =&gt; experimentsFromState,(experimentsFromProps, experimentsFromState) =&gt; ({...experimentsFromProps,...experimentsFromState,}),
);
...
render() {...const finalExperiments = getExperiments({experimentsFromProps: experiments,experimentsFromState: this.state.experiments,});return (&lt;WrappedComponent{...otherProps}experiments={finalExperiments}/&gt;);
}

问题的第二部分是类似的。我们使用了 getFilteredAmenities 函数,该函数将数组作为其第一个参数,并返回该数组的过滤版本,类似于:

function getFilteredAmenities(amenities) {return amenities.filter(shouldDisplayAmenity);
}

虽然看起来没什么问题,但每次运行时,即使结果相同也会创建一个新的数组实例,任何 pure components 只要把该数组当做参数,就不能得到优化。我通过引入reselect来缓存这个过滤器,从而解决了这个问题。到这里就没有火焰图了,因为整个重新渲染完全消失了! ?

除此以外可能还有更多优化机会(例如CSS containment),但滚动性能已经有了比较大的改善!

修复后的 Airbnb 房源页面的滚动性能

点击操作

继续与页面进行更多的交互,我明显感觉到在点击评论中的“Helpful”按钮时存在延迟✈️。

我的直觉是单击此按钮会导致页面上的所有评论都会被重新渲染。看一下火焰图,和我预计的一样:

重新渲染 ReviewsContent,耗时 42.38 毫秒

在几个地方使用 React.PureComponent 之后,页面更新变得更加高效了。

重新渲染 ReviewsContent,耗时 12.38 毫秒

输入操作

再回到服务端/客户端不匹配这一老问题,我注意到在输入框里打字时反应很迟钝。

分析后发现,每次按键操作都会导致整个评论区头部以及每条评论全部被重新渲染! ?这是在逗我吗?

重新渲染 Redux 连接的 ReviewsContainer,耗时 61.32 毫秒

为了解决这个问题,我将评论区头部的一部分提取出来作为组件,这样我就可以将其作为 React.PureComponent,然后再把这几个 React.PureComponents 分散在树上。这使得每次按键操作仅重新渲染需要重新渲染的组件:输入框。

重新渲染 ReviewsHeader,耗时 3.18 毫秒

我们学到了什么?

  • 我们希望页面快速启动并保持流畅。

  • 这意味着我们需要关注的不仅仅是用户发起请求到页面可交互的时间(Time to Interactive),还需要分析页面上的交互动作,例如滚动,点击和输入。

  • React.PureComponent 和 reselect 是 React 应用优化过程中非常有用的两个工具。

  • 当实例变量完全满足你的需求时,就应该避免使用 React state。

  • 虽然 React 功能强大,但也很容易写出影响性能的代码。

  • 培养分析、优化、再次分析的习惯。

关于本文
译者:@Yvan Zhong
作者:@Joe Lencioni
校对:Lawrence Lin
译文:https://zhuanlan.zhihu.com/p/44404836
原文:
https://medium.com/airbnb-engineering/recent-web-performance-fixes-on-airbnb-listing-pages-6cd8d93df6f4

Airbnb 爱彼迎房源详情页中的 React 性能优化相关推荐

  1. Airbnb爱彼迎推出看得见“春色”的房源

    春分将至,人们对春暖花开.拾翠踏青的渴望从未如此强烈.打开窗户,温柔的春风令人心生无限遐思,虽然身未动,但心已远行.Airbnb爱彼迎推出八个看得见"春色"的房源,大家可以一同感受 ...

  2. Airbnb(爱彼迎)产品分析报告

    Airbnb(爱彼迎)产品分析报告 一.Airbnb背景 Airbnb成立于2008年,一家联系旅游人士和家有空房出租的房主的服务型网站,它可以为用户提供多样的住宿信息.用户可通过网络或手机应用程序发 ...

  3. 爱彼迎房源数超过全球六大酒店集团房间总量

    Airbnb爱彼迎的愿景是创造一个"家在四方"的世界,致力于帮助旅行者们在世界各地旅行时找到归属感.近日,Airbnb爱彼迎宣布,其房东已经在全球超过600万套房源中开门迎客,房源 ...

  4. Airbnb爱彼迎2021年第一季度营收同比增长5%

    北京时间2021年5月14日,Airbnb爱彼迎发布了2021年第一季度财报."我对我们的强劲业绩感到非常自豪.尽管我们的传统优势领域,即城市旅行和跨境旅行尚未完全恢复,我们的营收水平已经超 ...

  5. 三个月来美国又有一万家餐馆因疫情倒闭或关闭;爱彼迎帮助在危机中的人寻找临时住宿 | 美通企业日报...

    今日看点:三个月来美国又有一万家餐馆因疫情倒闭或关闭.爱彼迎推出非营利组织Airbnb.org帮助紧急寻找临时住宿.默克公司扩大美国生命科学产能.前辉瑞普强中国区首席运营官黄海出任菲吉乐科全球CEO. ...

  6. DeepFM在贝壳房源详情页推荐场景的实践

    上一篇文章<wide&deep 在贝壳推荐场景的实践[1]>中,我们介绍了贝壳首页推荐展位使用的 Wide & Deep 模型,本文向大家介绍贝壳房源详情页推荐展位使用的 ...

  7. airbnb 爱彼迎开源 Epoxy 优化使用 RecyclerView

    airbnb 爱彼迎开源 Epoxy 优化使用 RecyclerView 一.为什么要使用Epoxy RecyclerView 众所周知是在listview和gridview基础上优化缺点,提炼出的一 ...

  8. 如何在宝贝详情页中制作一张图片多个链接

    详情页中一张图加链接都很简单,问题来了,如何在一张图的不同地方加不同链接呢?分享给大家,一起看看吧! 工具:Adobe Dreamweaver CS5 1.打开软件 首先打开Adobe Dreamwe ...

  9. JS实现放大镜的效果 —— 仿京东详情页中的产品放大效果

    案例简述 这次案列是模仿京产品详情页中,产品图放大的效果. 简单说下效果的具体步骤: 1.鼠标经过预览区时,遮盖层和产品大图显示:鼠标离开则隐藏 2.遮盖层跟随鼠标进行移动,并且遮盖层只在预览区域内移 ...

最新文章

  1. 电子商务网站的经验教训
  2. linux c++ 得到 指定进程名 线程数
  3. Mac下布置appium环境
  4. Qt 控件渐变隐藏消失
  5. 关于知识图谱,各路大神最近都在读哪些论文?
  6. oracle 合并重复数据_三天三夜整理出来的数据库常见的面试题,让你直接拿走...
  7. 算法分类整理+模板②:字符串处理
  8. LeetCode 股票买卖问题
  9. React优化性能的经验教训
  10. hdu 2896 病毒侵袭
  11. poj Alice's Chance(最大流解题)
  12. 日程表|第8届高等学校计算机程序设计课程论坛
  13. VS2010 error LNK2019: 无法解析的外部符号
  14. CREO:CREO软件之零件【模型】扫描之扫描、螺旋扫描、可变剖面扫描、扫描混合、混合、边界混合、可变剖面扫描的简介及其使用方法(图文教程)之详细攻略
  15. kali PHP网站渗透,小白日记35:kali渗透测试之Web渗透
  16. Head First 深入浅出系列 电子书
  17. 为什么证券投资是世界上最难成功的行业
  18. snap install cloudcompare碰到问题
  19. 关于麦克风波束成形的基本原理
  20. 掌阅群分享技术点收集(app性能优化专攻)

热门文章

  1. python制作图例_python plt可视化——打印特殊符号和制作图例代码
  2. Foxit Reader
  3. 微服务(三) 【手摸手带你搭建Spring Cloud】 Ribbon 什么是负载均衡?spring cloud如何实现负载均衡?ribbon负载均衡有几种策略?Ribbon是什么?
  4. ORA-02063: preceding line from DBLINK
  5. 微信小程序 获取nickName为 “微信用户” 而且 头像 为null
  6. 查看B站UP开播状态(自己关注的)
  7. 【Tanh的标量实现】
  8. Windows Docker Desktop容器自动化管理
  9. 【数据结构与算法】 常用的十大算法
  10. 盲盒商城小程序开发功能列表