如何全方位优化你的超大型 React 应用
(给前端大全加星标,提升前端技能)
作者: 前端巅峰 公号 / Peter 谭金杰 (本文来自作者投稿)
React
为了大型应用而生,Electron
和React-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
形式,大都依赖框架,Vue
和React
之类。一旦使用这类型技术架构,状态数据集中管理,单向数据流,不可变数据,路由懒加载,按需加载组件,适当的缓存机制(PWA
技术),细致拆分组件,单一数据来源刷新组件,这些都是我们可以精细化的方向。往往纯CSR
的单页面应用一般不会太复杂,所以这里不引入PWA
和web 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
,这是一个遗憾,这里需要配合wepback4
的optimization
配置,进行代码分割
Tips:
这里需要下载支持动态import
的babel预设包 @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
就可以拿到数据页面访问
服务端渲染核心,保证代码在服务端运行一次,将redux
的store
状态树中的数据一起返回给客户端,客户端脱水,渲染。保证它们的状态数据和路由一致,就可以说是成功了。必须要客户端和服务端代码和数据一致性,否则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.js
和javaScript
都不适合做大量计算工作,这点有目共睹,尤其是js
引擎和GUI
渲染线程互斥的情况存在。
充分合理利用React
的Feber
架构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的回调,如下图所示:
使用preload
,prefetch
,dns-prefetch
等指定提前请求指定文件,或者根据情况,浏览器自行决定是否提前dns
预解析或者按需请求某些资源。
这里也可以
webpack4
插件实现,目前京东在使用这个方案~
对指定js
文件延迟加载~
给
script
标签,加上async
标签,遇到此标签,先去请求,但是不阻塞解析html
等文件~,请求回来就立马加载
给
script
标签,加上defer
标签,延迟加载,但是必须在所有脚本加载完毕后才会加载它,但是这个标签有bug
,不确定能否准时加载。一般只给一个
写这篇时间太耗时间,
React-native
的以及一些细节,后面再补充
觉得本文对你有帮助?请分享给更多人
关注「前端大全」加星标,提升前端技能
好文章,我在看❤️