可以说 React 为Web开发者带来了全新的开发模式,而在各类新功能下,如何达到性能最优仍是我们需要关心的。今天做一次精读尝试,原文地址在文末,话不多说,先呈上一份性能清单:1. 测量组件级渲染性能Chrome DevTools Performance 面板React DevTools profiler 面板2. 避免非必要的组件重复渲染尽量使用shouldComponentUpdateClass 组件使用PureComponent功能组件使用React.memo记住 Redux selectors(比如使用reselect)虚拟化超长列表(比如使用react-window)3. 使用 Lighthouse 测量App级性能4. 提升APP级性能如果没有使用服务端渲染,则使用React.lazy分割组件如果使用了服务端渲染,则使用loadable-components之类的库来分割组件使用 service worker 来缓存需要的文件,Workbox 可以帮到你如果使用了服务端渲染,使用流式传输(使用renderToNodeStream或renderToStaticNodeStream)无法使用 SSR?使用react-snap等方案进行预渲染(Pre-render)如果用到 CSS-in-JS 库,将关键路径样式解析出来保障应用可用性,考虑使用React A11y或react-axe等库如果用户需要通过设备主屏幕访问站点,增加 web app manifest对于 React 应用,我们主要关注两个性能维度:组件渲染性能 与 页面加载性能,由于 React 的核心在于组件设计,那先从组件性能讲起。测量组件级性能React 熟为人知的“Virtual DOM”,是建立在高效调和(reconciliation)算法基础上的,其基于一定约定假设,将虚拟 DOM Diff 时间复杂度从O(n3)降为O(n)。虽然这些 React 内部实现不要求大家都理解,在小型应用中性能也不足以成为瓶颈,但性能优化本来就是量变到质变的过程,因此让我们从测量组件性能工具做起。使用 Chrome 开发者工具测量性能React 使用 User Timing API 收集各生命周期耗时,为避免测量本身带来的性能影响,性能采集仅在开发模式有效。说实话,这类火焰图在视觉上有很强直观性,但缺少的有效调试信息,因此 React Devtools 提供了更为强大的能力。使用 React DevTools Profiler 分析性能React 16.5 开始使用 Profiler API 收集组件渲染耗时,以独立Tab形式呈现在 React DevTools 中。它的使用类似于 Chrome DevTools Performance,通过录制来决定收集数据范围。React DevTools Profiler 示例相比 Chrome DevTools Performance 中呈现的 Timing 信息,React DevTools Profiler 提供了更多辅助定位性能瓶颈的组件级信息,这里简单说下几个亮点:以 commit 维度记录信息。熟悉 React 内部原理的同学知道,React 生命周期中有个 Commit 阶段,React DevTools Profiler 会以每次 commit 维度记录渲染相关信息,在右侧进行展示。具体组件状态信息。左侧的火焰图对应了组件层级结构,以不同颜色区分组件渲染次数,高亮重复渲染的组件。点击组件后,右侧会展示组件具体渲染次数,以及当时的 state 与 props。简单的统计能力。除了火焰图,工具还有排名(Ranked)和交互(Interactions)两个维度统计,帮助更快的定位组件瓶颈。总体上 Profiler 工具使用简单,没什么门槛,接下来介绍优化组件渲染的相关技术。避免非必要的组件重复渲染去除无用的重复渲染,方案因场景各异:使用 shouldComponentUpdateshouldComponentUpdate(nextProps, nextState) { // 仅在确定条件下返回 true}Class 组件使用 PureComponentimport React, { PureComponent } from ‘react’;class AvatarComponent extends PureComponent {}功能组件使用 memoimport React, { memo } from ‘react’;const AvatarComponent = memo(props => {});记住 Redux selectors(比如使用 reselect)虚拟化超长列表(比如使用 react-window)测量 App 级性能除了 DOM 级的渲染性能,还有更高层面的应用加载性能需要关注。这方面的性能工具属 Lighthouse 最有名了,我们可以通过 Node CLI、Chrome 扩展和 Chrome DevTools 的 Audits 面板用到它。Lighthouse 根据一系列性能规则,对目标页面进行检查,最终生成一份性能报告,给出未达标指标的改进建议。在 React 项目中,随着路由和组件的膨胀,很容易触发 Lighthouse 对 JavaScript 传输体积的检查规则(Avoid enormous network payloads)。在实践中,已有成熟的方案供我们使用——代码分割。代码分割进行代码分割的一个方法是动态导入(dynamic imports): import(’lodash.sortby’) .then(module => module.default) .then(module => doSomethingCool(module))这里的 import 语法像是函数调用,允许异步加载模块并通过 Promise 返回。上面代码动态获取了 lodash sortby 方法,紧接着被后续代码使用。虽然动态导入目前仍处于 stage 3 阶段,Chrome and Safari 已经率先支持了,Webpack、Rollup 和 Parcel 也做好了支持。回到 React,组件级别的代码分割已经被良好地抽象,比如React.lazy:import React, { lazy } from ‘react’;const AvatarComponent = lazy(() => import(’./AvatarComponent’));然而这么做可能会导致用户可感知的加载延迟。对此,可以将Suspense组件配合React.lazy一起使用,“暂停”部分组件的渲染,通过渲染 Loading 组件,对仍在加载的组件进行降级处理:import React, { lazy, Suspense } from ‘react’;import LoadingComponent from ‘./LoadingComponent’;const AvatarComponent = lazy(() => import(’./AvatarComponent’));const PageComponent = () => ( <Suspense fallback={LoadingComponent}> <AvatarComponent /> </Suspense>)Suspense 还不支持 SSR,如果要在服务端渲染使用代码分割,可以使用loadable-components这样的库。另外如果需要在滚动场景做异步加载的同学,可以了解下 react-loadable-visibility。缓存Service Worker 就不重新介绍了,概括起来就是一个运行在浏览器后台的可编程代理,让我们对网络缓存更加可控。一个具体的使用场景是,通过控制缓存策略,来提升用户二次访问时的页面加载体验。这里主要是安利 Workbox 这个工具包,它能让我们更简单地使用 Service Worker,具体细节不做展开,在 PWA 的浪潮中,你的站点值得拥有。流式 SSR为了加快页面呈现,服务端渲染概念已经被大家接受和使用。为了最大限度复用服务端返回的 HTML,React 还提供了 hydrate() API。这时优化的目光投向了 TTI,流式渲染也应运而生,相对之前的renderToString API 返回 HTML 字符串,renderToNodeStream会返回 NodeReadable字节流。这样浏览器就能源源不断地获取到页面块,hydrate API 也很好地支持了流式处理,真的很强大。关于 SSR 更多信息,可以查看本专栏的《Web渲染那些事儿》。SSR 不行?预渲染来顶其实服务端渲染是个笼统的概念,由于现代页面大多都是动态的,因此每个请求可能都要在服务器上处理一遍。然而纯服务端渲染与纯客户端渲染之间,是存在中间地带的。虽然页面是通过组件模式进行开发,但页面内容可能是静态的,只要生成一次就行,这就是预渲染(Prerendering)或静态渲染的由来。这里介绍一个基于 Puppeteer 的预渲染方案 react-snap,它能让你更简单地进行预渲染页面。提取关键 CSS-in-JS 样式出于各种原因,有些开发者会使用 emotion 及 styled-components 等 CSS-in-JS 库,但如果不注意,会导致样式都在运行时解析,也就是导致页面会闪过无样式的瞬间。如果在移动设备或弱网络场景下,体验就很糟糕。上面提到的 SSR 更是如此,因为在客户端JS加载之前,SSR 返回的无样式 DOM 已经开始渲染了。优化的做法就是将这些关键样式提取出来,好在 emotion 和 styled-components 都原生支持将样式提取到可读流中,流式 SSR 也不用担心闪屏情况了。杂项接下几项关于提升开发者体验,并助于减少繁琐的编码。编写更少代码 = 传输更少代码 = 更快的网页加载原子 CSS原子样式的理念是定义单一作用的 class,以达到灵活组合样式的目的。看个简单的例子:<button class=“bg-blue”>Click Me</button>bg-blue 定义了蓝色背景色,作用在 button 上可令其应用这条规则。如果要给它加个 padding,可以设置单独负责 padding 的 class:<button class=“bg-blue pa2”>Click Me</button>虽然会多写几个 css class,但可以不用再去编辑复杂的 CSS 文件了,如果你不想自己维护一套样式规范,可以直接用开源的 Tachyons 方案。组件级别还有 tachyons-components 这样的方案,个人觉得还不太成熟,这里不做展开。整体来看原子 CSS 比较适用于样式风格简单统一的场景,让开发者聚焦 JS 部分,随时修改样式而不用关心样式继承方面的影响,另一个好处是 CSS 可以长期缓存,基本不需要更新。出于性能考虑,页面首次加载会被统一样式的 CSS 阻塞,看了下gzip后有10KB大小,还是看场景应用吧。HooksHooks 允许以功能组件实现以前只有class组件才能实现的功能,比如对state的操作:import { useState } from ‘react’;function AvatarComponent() { const [name, setName] = useState(‘Houssein’); return ( <React.Fragment> <div> <p>This is a picture of {name}</p> <img src=“avatar.png” /> </div> <button onClick={() => setName(‘a banana’)}> Fix name </button> </React.Fragment> );}除了 React 提供的 useState 和 useEffect,可以自定义 hooks 来复用跨组件的逻辑。在此之前要实现该功能,会用到 recompose 这个库,Hooks 出现后就可以退出历史舞台了。(真实情况是 recompose 的作者加入了 React Team,并推出了 Hooks)虽然 Hooks 的定位是解决代码架构问题,但确实也在加载性能方面做出了贡献。虽然 Hooks 功能相关代码为 React 增加了1.5KB(gzip后),但 Hooks 代码比 class 组件代码更易压缩,因此可以减小一些 JS 包大小。总结像 React 这样拥有广泛开发者的开源项目,有两样事可以期待:优化其 API,令构建应用更加容易开源社区贡献第三方库,令构建应用更加容易“令构建应用更加容易”可以指很多方面,让开发者做的更少、页面性能更高是其中之一。延伸阅读progressive reactReact as a UI Runtime Debugging React performance with React 16 and Chrome Devtools Introducing the React Profiler make loadable-components work with SSR 《Web渲染那些事儿》
...