如何全方位优化你的超大型React应用 【原创精读】

 阅读约 29 分钟

React为了大型应用而生,ElectronReact-native赋予了它构建移动端跨平台App和桌面应用的能力,Taro则赋予了它一次编写,生成多种平台小程序和React-native应用的能力,这里特意说下 Taro,它是国产,文档写得比较不错,而且它的升级速度比较快,有issue我看也会及时解决,他们的维护人员还是非常敬业的!
  • Tips:本文某些知识点如果介绍不对或者不全的地方欢迎指出,本文可能内容比较多,阅读时间花费比较长,但是希望你可以认真看下去,可以的话最好手把手去实现一些code,本文所有代码均手写。

本文会从原生浏览器环境,到跨平台开发逐渐去深入介绍,先给一些资料

  • 手写React优化脚手架带项目
  • react-ssr的源码
  • 手写Node.js原生静态资源服务器
  • 跨平台Electron的demo

原生浏览器环境:

  • 原生浏览器环境其实是最考验前端工程师能力的编程环境,因为我们前端大部分一开始面向浏览器编程,现在很多很多工作5-10年的前端,性能面板API都不知道用,怎么看调用函数分析耗时都不知道,这也是最近面试的情况,觉得有人说35岁失业的情况,是普遍存在,但是很大部分是你在混啊兄弟。
原生浏览器环境中使用React框架,比较常见的是制作单页面SPA应用:
原生的SPA应用,分以下几种:
  • CSR渲染(客户端渲染)
  • SSR渲染(服务端渲染)
  • 混合渲染(预渲染,webpack的插件预渲染,Next.js的约定式路由SSR,或者使用Node.js做中间件,做部分SSR,加快首屏渲染,或者指定路由SSR.)
下面会分别仔细介绍这几种渲染形式的精细化渲染,以及优缺点:

CSR渲染

  • 客户端请求RestFul接口,接口吐回静态资源文件
  • Node.js实现代码
  • 客户端收到一个HTML文件,和若干个CSS文件,以及多个javaScript文件
  • 用户输入了url地址栏然后客户端返回静态文件,客户端开始解析
  • 客户端解析文件,js代码动态生成页面。(这也是为什么说单页面应用的SEO不友好的原因,初始它只是一个空的div标签的HTML文件)
  • 判断一个页面是不是CSR,很大程度上可以根据右键点开查看页面元素,如果只有一个空的div标签,那么大概率可以说是单页面,CSR,客户端渲染的网页。

CSR的应用,如何精细化渲染呢?

单页面采取CSR形式,大都依赖框架,VueReact之类。一旦使用这类型技术架构,状态数据集中管理,单向数据流,不可变数据,路由懒加载,按需加载组件,适当的缓存机制(PWA技术),细致拆分组件,单一数据来源刷新组件,这些都是我们可以精细化的方向。往往纯CSR的单页面应用一般不会太复杂,所以这里不引入PWAweb work等等,在后面复杂的跨平台应用中我会将那些技术一拥而上。

  • 单一数据来源决定组件是否刷新是精细化最重要的方向。

一旦业务逻辑非常复杂的情况下,假设我们使用的是dva集中状态管理,同时连接这么多的状态树模块,那么可能会造成状态树模块中任意的数据刷新导致这个组件被刷新,但是其实这个组件此时是不需要刷新的。
  • 这里可以将需要的状态通过根组件用props传入,精确刷新的来源,单一可变数据来源追溯性强,也更方便debug
  • 单向数据流不可变数据,通过immutable.js这个库实现

不可变数据,数据共享,持久化存储,通过is比较,每次map生成的都是唯一的 ,它们比较的是codehash的值,性能比通过递归或者直接比较强很多。在PureComponent浅比较不好用的时候
  • 一般的组件,使用PureComponent减少重复渲染即可
  • PureComponent,平时我们创建 React 组件一般是继承于 Component,而 PureComponent 相当于是一个更纯净的 Component,对更新前后的数据进行了一次浅比较。只有在数据真正发生改变时,才会对组件重新进行 render。因此可以大大提高组件的性能。
  • PureComponent部分源码,其实就是浅比较,只不过对一些特殊值进行了判断

这里特别注意,为什么使用immutable.js和pureComponent,因为React一旦根组件被刷新,会自上而下逐渐刷新整个子孙组件,这样性能损耗重复渲染就会多出很多
所以我们不仅要单一数据来源控制组件刷新,偶尔还需要在shouldComponentUpdate中对比nextProps和this.props 以及this.state以及nextState
  • 路由懒加载+code-spliting,加快首屏渲染,也可以减轻服务器压力,因为很多人可能访问你的网页并不会看某些路由的内容
  • 使用react-loadable,支持SSR,非常推荐,官方的lazy不支持SSR,这是一个遗憾,这里需要配合wepback4optimization配置,进行代码分割
Tips:这里需要下载支持动态importbabel预设包 @babel/plugin-syntax-dynamic-import ,它支持动态倒入组件

  • 好了,现在路由懒加载组件以及代码分割已经做好了,而且它支持SSR。非常棒
  • 由于纯CSR的网页一般不是很复杂,这里再介绍一个方面,那就是,能不用redux,dva等集中状态管理的状态就不上状态树,实践证明,频繁更新状态树对用户体验来说是影响非常大的。这个异步的过程,更耗时。远不如支持通过props等方式进行组件间通信,原则上除了很多组件共享的数据才上状态树,否则都采用其他方式进行通信。

SSR,服务端渲染:

服务端渲染可以分为:
纯服务端渲染,如jade,tempalte,ejs等模板引擎进行渲染,然后返回给前端对应的HTML文件
  • 这里也使用Node.js+express框架
混合渲染,使用webpack4插件,预渲染指定路由,被指定的路由为SSR渲染,后台0代码实现

混合渲染,使用Node.js作为中间件,SSR指定的路由加快首屏渲染,当然CSS也可以服务端渲染,动态Title和meta标签,更好的SEO优化,这里Node.js还可以同时处理数据,减轻前端的计算负担。
  • 我觉得掘金上的神三元那篇文章就写得很好,后面我自己去逐步实现了一次,感觉对SSR对理解更为透彻,加上本来就每天在写Node.js,还会一点Next,Nuxt,服务端渲染,觉得大同小异。
  • 服务端渲染本质,在服务端把代码运行一次,将数据提前请求回来,返回运行后的html文件,客户端接到文件后,拉取js代码,代码注水,然后显示,脱水,js接管页面。
  • 同构直出代码,可以大大降低首屏渲染时间,经过实践,根据不同的内容和配置可以缩短40%-65%时间,但是服务端渲染会给服务器带来压力,所以折中根据情况使用。
  • 以下是一个最简单的服务端渲染,服务端直接吐拼接后的html结构字符串:
只要客户端访问localhost:3000就可以拿到数据页面访问
服务端渲染核心,保证代码在服务端运行一次,将reduxstore状态树中的数据一起返回给客户端,客户端脱水,渲染。保证它们的状态数据和路由一致,就可以说是成功了。必须要客户端和服务端代码和数据一致性,否则SSR就算失败。

render函数:

  • 数据注水,脱水,保持客户端和服务端store的一致性。
上面返回的script标签,里面已经注水,将在服务端获取到的数据给到了全局window下的context属性,在初始化客户端store时候我们给它脱水。初始化渲染使用服务端获取的数据~

  • 这里注意,在组件的componentDidMount生命周期中发送ajax等获取数据时候,先判断下状态树中有没有数据,如果有数据,那么就不要重复发送请求,导致资源浪费。
  • 多层级路由SSR

  • 入口文件路由部分改成:

  • 后续可能有利用loader进行CSS的服务端渲染以及helmet的动态meta, title标签进行SEO优化等,今天时间紧促,就不继续写SSR了。

构建Electron极度复杂,超大数据的应用。

需要用到技术,sqlite,PWA,web work,原生Node.js,react-window,react-lazyload,C++插件等
  • 第一个提到的是sqlite,嵌入式关系型数据库,轻量型无入侵性,标准的sql语句,这里不做过多介绍。
  • PWA,渐进性式web应用,这里使用webpack4的插件,进行快速使用,对于一些数据内容不需要存储数据库的,但是却想要一次拉取,多次复用,那么可以使用这个配置

serverce work也有它的一套生命周期

  • 通常我们如果要使用Service Worker基本就是以下几个步骤:
  • 首先我们需要在页面的 JavaScript 主线程中使用 serviceWorkerContainer.register() 来注册 Service Worker ,在注册的过程中,浏览器会在后台启动尝试 Service Worker 的安装步骤。
  • 如果注册成功,Service Worker 在 ServiceWorkerGlobalScope 环境中运行;这是一个特殊的 worker context,与主脚本的运行线程相独立,同时也没有访问 DOM 的能力。
  • 后台开始安装步骤, 通常在安装的过程中需要缓存一些静态资源。如果所有的资源成功缓存则安装成功,如果有任何静态资源缓存失败则安装失败,在这里失败的不要紧,会自动继续安装直到安装成功,如果安装不成功无法进行下一步 — 激活 Service Worker。
  • 开始激活 Service Worker,必须要在 Service Worker 安装成功之后,才能开始激活步骤,当 Service Worker 安装完成后,会接收到一个激活事件(activate event)。激活事件的处理函数中,主要操作是清理旧版本的 Service Worker 脚本中使用资源。
  • 激活成功后 Service Worker 可以控制页面了,但是只针对在成功注册了 Service Worker 后打开的页面。也就是说,页面打开时有没有 Service Worker,决定了接下来页面的生命周期内受不受 Service Worker 控制。所以,只有当页面刷新后,之前不受 Service Worker 控制的页面才有可能被控制起来。
直接上代码,存储所有js文件和图片 //实际的存储根据自身需要,并不是越多越好。

  • PWA并不仅仅这些功能,它的功能非常强大,有兴趣的可以去lavas看看,PWA技术对于经常访问的老客户来说,首屏渲染提升非常大,特别在移动端,可以添加到桌面保存。666啊~,在pc端更多的是缓存处理文件~
  • 使用react-lazyload,懒加载你的视窗初始看不见的组件或者图片

  • 懒加载组件

大数据React渲染,拥有让应用拥有60FPS -非常核心的一点优化

  • List长列表

  • react-virtualized-auto-sizer和windowScroll配合一起使用,达到页面复杂效果+大数据渲染保持60FPS。上面的官网里有介绍这些组件~

高计算量的工作交给web wrok线程

  • 这段代码中变量first和second代表2个input元素;它们当中任意一个的值发生改变时,myWorker.postMessage([first.value,second.value])会将这2个值组成数组发送给worker。你可以在消息中发送许多你想发送的东西。
  • 在worker中接收到消息后,我们可以写这样一个事件处理函数代码作为响应(worker.js):

  • onmessage处理函数允许我们在任何时刻,一旦接收到消息就可以执行一些代码,代码中消息本身作为事件的data属性进行使用。这里我们简单的对这2个数字作乘法处理并再次使用postMessage()方法,将结果回传给主线程。
  • 回到主线程,我们再次使用onmessage以响应worker回传的消息:

  • 在这里我们获取消息事件的data,并且将它设置为result的textContent,所以用户可以直接看到运算的结果。
  • 注意:在主线程中使用时,onmessage和postMessage() 必须挂在worker对象上,而在worker中使用时不用这样做。原因是,在worker内部,worker是有效的全局作用域。
  • 注意:当一个消息在主线程和worker之间传递时,它被复制或者转移了,而不是共享。
开启web work线程,其实也会损耗一定的主线程的性能,但是大量计算的工作交给它也未尝不可,其实Node.jsjavaScript都不适合做大量计算工作,这点有目共睹,尤其是js引擎和GUI渲染线程互斥的情况存在。

充分合理利用ReactFeber架构diff算法优化项目

  • requestAnimationFrame调用高优先级任务,中断调度阶段的遍历,由于React的新版本调度阶段是拥有三根指针的可中断的链表遍历,所以这样既不影响下面的遍历,也不影响用户交互等行为。

使用requestAnimationFrame也可以更好的让浏览器保持60帧的动画
  • 使用requestAnimationFrame,当页面处于未激活的状态下,该页面的屏幕刷新任务会被系统暂停,由于requestAnimationFrame保持和屏幕刷新同步执行,所以也会被暂停。当页面被激活时,动画从上次停留的地方继续执行,节约 CPU 开销。
  • 一个刷新间隔内函数执行多次时没有意义的,因为显示器每 16.7ms 刷新一次,多次绘制并不会在屏幕上体现出来
  • 在高频事件(resize,scroll等)中,使用requestAnimationFrame可以防止在一个刷新间隔内发生多次函数执行,这样保证了流畅性,也节省了函数执行的开销 某些情况下可以直接使用requestAnimationFrame替代 Throttle 函数,都是限制回调函数执行的频率
  • requestIdleCallback,这个API目前兼容性不太好,但是在Electron开发中,可以使用,两者还是有区别的,而且这两个api用好了可以解决很多复杂情况下的问题~。当然你也可以用上面的api封装这个api,也并不是很复杂。
  • 当关注用户体验,不希望因为一些不重要的任务(如统计上报)导致用户感觉到卡顿的话,就应该考虑使用requestIdleCallback。因为requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态。

  • 图中一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工作。假如某一帧里面要执行的任务不多,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调,如下图所示:

使用preloadprefetch,dns-prefetch等指定提前请求指定文件,或者根据情况,浏览器自行决定是否提前dns预解析或者按需请求某些资源。

  • 这里也可以webpack4插件实现,目前京东在使用这个方案~

对指定js文件延迟加载~

  • script标签,加上async标签,遇到此标签,先去请求,但是不阻塞解析html等文件~,请求回来就立马加载

  • script标签,加上defer标签,延迟加载,但是必须在所有脚本加载完毕后才会加载它,但是这个标签有bug,不确定能否准时加载。一般只给一个

写这篇时间太耗时间,React-native的以及一些细节,后面再补充

原创不易,感觉有收获,请帮忙关注一下公众号,点个在看,谢谢

阅读 2.7k更新于 1月9日

推荐阅读
前端巅峰
用户专栏

注重前端性能优化和前沿技术,重型跨平台开发,即时通讯技术等。 欢迎关注微信公众号:前端巅峰

11618 人关注
50 篇文章
专栏主页
目录