Twitter Lite以及大规模的高性能React渐进式网络应用

Twitter Lite以及大规模的高性能React渐进式网络应用

原文:Twitter Lite and High Performance React Progressive Web Apps at Scale

译者:neal1991

welcome to star my articles-translator , providing you advanced articles translation. Any suggestion, please issue or contact me

LICENSE: MIT

让我们一起来了解世界最大的React.js PWA, Twitter Lite之中常见的和不太常见的性能瓶颈。

创建一个高速的web应用包含非常多方面,包含:时间花费在什么地方。理解其发生的原因而且应用潜在的解决方式。不幸的是。从来就没有一个高速的修复方法。性能是一个持续的问题,涉及到须要对须要提高的内容的持续观察和检測。在Twitter Lite中。我们在非常多方面进行了一些小的提升:从初始载入时间搭配React组件的渲染(以及避免再次渲染)到图像的载入等等。大多数的变化往往是非常小的,当全部的变化叠加在一起让我们开发出了最大的以及最快的渐进式web应用

timeline

在继续阅读之前:

假设你才開始观測而且提升你的web应用,我强烈推荐你学习怎样阅读帧图,假设你还不知道怎样去做的话。

以下的每一个章节包含样例的 Chrome里面的开发人员工具timeline记录的截图。为了让结果更清晰,我强调每一对样例坏的(左图)和好的(右图)进行对照(译者注:由于markdown图片显示的问题。因此原文的左右图在本文中是上图和下图)。

对于timeline和帧图特别的一点:由于我们针对的是非常多种的手机设备,我们一般都会在一个模拟的环境中记录这些数据:比5x要慢的CPU以及3G的网络连接。这个不仅更现实,而且还会让问题更easy发现。

经过非常多讨论,我们最终通过路由将公共区域分解成独立的块(样例例如以下)。当我们收件箱收到代码审查的通知的那一天最终来了:

const plugins = [
  // 提取vendor和webpack模块的manifest
  new webpack.optimiza.CommonChunkPlugin({
    names: [ 'vendor', 'manifest'],
    minChunks: Infinity
  }),
  // 从全部的块中提取公共模块(不须要'name'属性)
  mew webpack.optimize.CommonChunkPlugin({
    async: true,
    children: true,
    minChunks: 4
  })
];

加入细粒度。基于路由的代码切割。为了加快初始化和主页timeline渲染。app的总体大小可能会更大。文件会在sesiion期间内按需分块在40个代码块之中。–Nicolas Gallagher

bad

good

我们的原始设置(上面的图)载入我们基本的压缩包花费了超过5秒的时间,然而在通过路由和公共代码块进行代码切割之后(以下的图),就只花费了3秒的时间(在模拟的3G网络中)。

我们在性能优化初期专注完毕了这一点,可是这一点变化对于Google的Lighthouse这一web应用审查工具的时候得到的结果却有了显著的变化:

lighthouse

避免函数导致的Jank

在我们无限滚动的timeline的众多迭代中。我们使用不同的方式来计算你的滚动位置和方向,从而决定我们是否须要API来展示很多其它的Tweet。

直到近期,我们使用了react-waypoint。这在我们项目中工作的非常好。然而,为了尽可能追求我们app的主要基础组件之中的一个的最佳性能,他的速度还不足够快。

Wayponints通过计算非常多元素不同的高度,宽度以及位置来决定你如今的滚动位置。以及你距离终点的距离。以及你滚动的方向。全部的这些信息都是实用的,可是由于它是在在每一次滚动事件发生的,因此这是有代价的:带来的计算会造成非常多的jank。

可是首先,我们必须明确这意味着什么。假设开发人员工具告诉我们这里有一个”jank”。

大多的设备屏幕每秒会刷新60次。假设有动画或者转换执行。或者用户正在滚动页面,浏览器须要匹配设备的刷新率,并为每一个屏幕刷新加入一个新的图片或者帧。

这些帧中每一帧花费的时间都超过16ms(1秒/60=16.66ms)。然而,实际上浏览器还有其它的工作。所以全部的工作须要在10ms之内完毕。

当你不能满足这个要求的话,帧率就会下降,内容在屏幕上的显示就会断断续续。这通常成为jank。它对用户体验会造成负面的影响。–Paul Lewis 关于渲染性能

之后,我们开发了一个名为VirtualScroller的新的无限滚动的组件。有了这个新的组件,我们确切知道在不论什么给定时间,什么片段的Tweet被渲染到时间轴上,从而避免在视觉呈现上导致的昂贵的计算。

jank

jank1

通过避免函数调用带来的额外的jank。滚动Tweet的timeline看起来更加无缝,给我们一种更丰富,更原生的体验。虽然说这可能会更好,可是这样的变化对于timeline的滚动的平滑性带来和显著的提升。

这个是一个非常好的提醒,对于我们研究性能的时候。每一小点都非常重要。

使用更小的图片

我们開始推动在Twitter Lite上使用较少的带宽,通过和多个团队合作,我们能够在CDN上获得新的更小尺寸的图片。事实证明,通过减小图片的大小,我们只会渲染我们须要展示的(在尺寸和质量方面),我们发现不仅能够降低带宽的使用,而且我们能够提升再浏览器中的性能,特别是在滚动带有大量图片Tweet的timeline的时候。

为了确定的更好的较小图片的性能。我们能够在Chrome开发人员工具里面观察光栅timeline。

在我们减小图片尺寸之前,对于一张图片的解码须要300ms甚至之上,例如以下图所看到的。这是图片下载之后的处理时间,可是是在它在页面展示之前。

当你滚动页面的时候。而且你的目标是60帧/秒的渲染标准时候,我们希望在16.777ms之内尽可能块地处理(1帧)。

将一张图片渲染到试图就须要将近18帧,这也太多了。

另外须要注意timeline的一点是:你能够看到这个基本的timeline一直都是堵塞的直到图片完毕解码(如空白所看到的)。这意味着我们在这有一个相当大的性能瓶颈!

image

image1

如今。在我们降低我们图片尺寸之后,我们观測到只须要一帧就能够解码我们最大的图片。

优化React

使用shouldComponentUpdate方法

对于优化React应用性能一个最常见的建议就是使用shouldComponentUpdate方法

我们尽可能在不论什么时候都做到这个一点。可是有时候有些东西总是会被遗漏。

example

上图中的一个组件总是会进行更新:在主屏timeline的时候点击爱心图标去赞一篇Tweet的时候。不论什么一个在屏幕上的Conversation组件都会又一次渲染。

在这个动画样例中,你能够看到浏览器须要对被绿色盒子注明的地方进行重绘。

由于我们针对的是Tweet以下的整个Conversation组件来进行更新action。

例如以下,你能够看到两个这个action的帧图。上面的没有使用shouldComponentUpdate,我们能够看到的它的整个树都被更新和又一次渲染,只不过是为了改变屏幕上某个地方的爱心的颜色。在加入shouldComponentUpdate(下图)之后,我们阻止了整个树进行更新而且避免了浪费0.1秒来执行不须要的处理。

shouldComponentUpdate

shouldComponentUpdate1

避免不必要的工作直到componentDidMount

这样的变化可能看起来是个人都会知道。可是在开发Twitter Lite这样的大型应用的时候。非常easy忘记这样的小事。

我们发如今我们代码中的非常多地方,为了对componentWillMountReact周期方法进行分析。我们花费大量的时间计算。

每一次我们做这个的时候。都会或多或少地堵塞组件的渲染.20ms或者90ms,这些时间非常快就累加到一起。

最初。我们尝试在tweets被渲染之前(timeline例如以下)记录哪些是在componentWillMount组件中被渲染到我们的数据分析服务中。

componentDidMount

componentDidMount1

通过将计算和网络调用移动到React组件的componentDisMount方法中,我们将主线程释放出来。而且降低了在渲染组件的时候不想要的jank。

避免dangerouslySetInnerHTML

在Twitter Lite中,我们使用SVG图标,由于这对于我们来说最便捷的而且缩放性最好的.不幸的是。在老的React版本号中,大多SVG属性在利用组件创建元素的时候是不支持的。因此。当我们開始写这个应用的时候,我们不得不使用dangerouslySetInnerHTML在React组件中来使用SVG图标。

比方,我们最初的HeartIcon可能看起来是这个样子的:

const HeartIcon = (props) => React.createElement('svg', {
  ...props,
  dangerouslySetInnerHTML: { __html: '<g><path d="M38.723 12c-7.187 0-11.16 7.306-11.723 8.131C26.437 19.306 22.504 12 15.277 12 8.791 12 3.533 18.163 3.533 24.647 3.533 39.964 21.891 55.907 27 56c5.109-.093 23.467-16.036 23.467-31.353C50.467 18.163 45.209 12 38.723 12z"></path></g>' },
  viewBox: '0 0 54 72'
});

不仅不鼓舞使用dangerouslySetInnerHTML,而且事实证明这个正是导致安装和渲染慢的原因。

svg

svg1

在分析上面地帧图之后,我们最初的代码(上面的)显示须要20ms的时间在一个慢的设备上安装这个action。即Tweet底部的SVG图标。虽然这看起来区别不是非常多,可是我们知道我们须要立即渲染。全部这些都在滚动无限的tweet的timeline,我们意识到这会非常的浪费时间。

自从React v15添加了对于大多数SVG属性的支持,我们想在前面来看一下假设我们避免使用dangerouslySetInnnerHTML会发生什么。看经过处理后的帧图(以下的),我们在每一次安装和渲染这些图标的时候能够节约60%。

如今。我们的SVG图标是简单的无状态的组件,不会使用“危急的”函数,而且安装速度提升了60%。它们看起来是这个样子的:

const HeartIcon = (props = {}) => (
  <svg {...props} viewBox='0 0 ${width} ${height}'>
    <g><path d='M38.723 12c-7.187 0-11.16 7.306-11.723 8.131C26.437 19.306 22.504 12 15.277 12 8.791 12 3.533 18.163 3.533 24.647 3.533 39.964 21.891 55.907 27 56c5.109-.093 23.467-16.036 23.467-31.353C50.467 18.163 45.209 12 38.723 12z'></path></g>
  </svg>
);

延迟渲染 当安装以及卸载非常多组件的时候

在慢的设备上,我们注意到须要非常长的时间我们的主浏览条才干够点击,这常常会导致我们点击左慈,假设可能第一次点击并没有注冊的话。

注意下图,能够看到主页的图标在点击之后差点儿花了2秒的时间来进行更新:

slow

不,这并非GIF在一个低的帧率下执行。

其实就是慢。可是,全部的主页屏幕的数据实际上已经载入了。为什么须要花费这么多的时间来显示呢?

事实证明安装和卸载大型组件树(比方Tweet的timeline)在React中是非常耗时的。

至少。我们希望去除掉这样的点击导航栏之后没有响应的感觉。为此,我们创建了一个小型的高阶组件:

import hoistStatics from 'hoist-non-react-statics';
import React from 'react';

/**
 * Allows two animation frames to complete to allow other components to update
 * and re-render before mounting and rendering an expensive `WrappedComponent`.
 */
export default function deferComponentRender(WrappedComponent) {
  class DeferredRenderWrapper extends React.Component {
    constructor(props, context) {
      super(props, context);
      this.state = { shouldRender: false };
    }

    componentDidMount() {
      window.requestAnimationFrame(() => {
        window.requestAnimationFrame(() => this.setState({ shouldRender: true }));
      });
    }

    render() {
      return this.state.shouldRender ? <WrappedComponent {...this.props} /> : null;
    }
  }

  return hoistStatics(DeferredRenderWrapper, WrappedComponent);
}

在应用到我们的主页timeline之后。我们能够看到导航栏一个比較快的响应速度,从而带来感官上一个大的提升。

const DeferredTimeline = deferComponentRender(HomeTimeline);
render(<DeferredTimeline />);

quick

优化Redux

避免存储State太频繁

虽然controlled components 被推荐使用。可是这也意味这每一次按键之后都须要进行更新而且又一次渲染。

虽然这对于一个3GHZ的台式机来说并不太难,但对于CPU数量有限的小型移动设备来说影响确实非常大的,尤其是从input中删除多个字符的时候。

为了保持正在撰写Tweet的值的时候同一时候计算剩余字符的数量。我们使用controlled组件。并将输入的当前值传递到每一个按键的Redux state。

例如以下(上面的),在一个典型的安卓5的设备上。每一次按键导致的更改可能会须要将近200ms的开销。这对于高速打字的人来说是非常痛苦的,这也让我们最终陷入了一个非常糟糕的状态,用户常常投诉他们的字符插入会移动到各个地方。从而导致混乱的句子。

redux

qucik

通过移除每一次按键下更新主Redux state的Tweet草稿state而且在本地保留Redux组件的状态。能够将开销降低50%。

将批量Actions合并成一个Dispatch

在Twitter Lite中。我们使用react-redux 配合redux来订阅我们组件的数据状态变化。我们通过使用 Normalizr 以及 combineReducers将大型的store来进行切割从而对我们的数据进一步优化。

这些都工作的非常好,避免了数据反复而且保持我们的store足够小。然而。每一次我们拿到新的数据的时候,我们必须分发多个action为了将它加入到合适的store中。

通过react-redux,这意味分派每一个action都会导致我们连接的组件(成为容器)又一次计算更改而且可能又一次渲染。

虽然我们使用了一个自己定义的middleware。还有其它的批量middleware。选择一个适合你的。或者你自己写一个。

说明使用批量action优点的最好方式是使用Chrome React Perf拓展。在初始载入之后。我们在后台pre-cache而且计算围堵的DM。

当这样的情况发生的时候,我们加入了非常多不同的实体(会话。用户,消息条目等等)。假设没有批量action的时候(以下的),你能够看到我们渲染组件的时间相对于批量action时候的时间对照是~16ms对~8ms。

action

action1

Service Workers

虽然Service Worker并没有在全部的浏览器得到支持,可是它还是Twitter Lite中非常重要的一个部分。

当Service Worker被支持的时候,我们用它做推送,预先缓存应用资源以及其它。不幸的是。作为一个想当新的技术。还有非常多性能提升方面的东西须要学习。

预先缓存资源

和大多数产品一样,Twitter Lite还远远没有完毕。

我们仍然在积极地开发它,加入新特性而且修复BUG以及让它执行得更快。这意味着我们常常须要部署新版本号的JavaScript资源。

不幸的是。这对于返回应用的用户来说是一个负担。由于他们须要又一次下载一大堆脚本文件不过为了浏览一个Tweet。

在ServiceWorker被支持的浏览器中,worker能够在你返回之前自己在后台自己主动更新,下载而且缓存不论什么改变的文件。我们从中获益。

因此这对用户意味着什么?差点儿是立即就能够载入应用,即使再在我们部署新版本号之后!

deploy1-r51dWgMIwA8-jFKno0_RcQ.png

在上面展示的(上面的)是没有使用ServiceWorker预先缓存资源,当前view中每一个资源都会强制从网络中载入假设返回应用的时候。在3G的网络环境下差点儿相同须要6秒的时间来完毕载入。然而。当资源被ServiceWorker预先缓存之后(以下的),相同在3G网络环境下只须要1.5秒就能够完毕页面的载入。

75%的提升!

延迟ServiceWorker注冊

在非常多应用中,在页面载入的时候立即注冊ServiceWorker是安全的:

<script>
window.navigator.serviceWorker.register('/sw.js');
</script>

当我们发送尽可能多的数据到浏览器来渲染一个看起来比較完整的页面,可是在Twitter Lite情况不一定就是这样的。我们可能不会发送足够的数据,或者你打开的页面不会支持数据从server全部获取。由于这样的或者那种的非常多限制,我们须要在初始页面载入之后立马发送一些API请求。

正常情况。这不会是一个问题。然而,假设浏览器还没有安装当前版本号的ServiceWorker,我们须要让它安装,随之而来就是对于JS,CSS以及图片资源预缓存的50个请求。

当我们使用简单的方法来立即注冊ServiceWorker的时候,我们能够看到网络连接发生在浏览器中,达到了并行请求的限制(上面的)。

1-jLT8J20RRfKY_oxcsAiswQ.png

1-poli81EjMdim8__g1HMioQ.png

通过延迟ServiceWorker的注冊直到我们完毕了额外的API,CSS以及图片资源的请求,我们能够让页面完毕渲染而且是响应式的,在以下的截图能够看到(以下的)。

总的来说,这是我们在Twitter Lite的开发过程中的众多性能提升的列表。

当然还会有很多其它的事情。我们希望能够继续分享我们发现的问题,以及我们克服问题所做的工作。

关于事实以及耕作React以及PWA的很多其它状态,能够在Twitter上关注以及我的团队

原文地址:https://www.cnblogs.com/mfmdaoyou/p/7382514.html