「react进阶」年终送给react开发者的八条优化建议(篇幅较长,占用20-30分钟)(上)

简介: 通过 React 性能优化

笔者是一个 react 重度爱好者,在工作之余,也看了不少的 react 文章, 写了很多 react 项目 ,接下来笔者讨论一下 React 性能优化的主要方向和一些工作中的小技巧。送人玫瑰,手留余香,阅读的朋友可以给笔者点赞,关注一波 。 陆续更新前端文章。

本文篇幅较长,将从
编译阶段 ->
路由阶段 ->
渲染阶段 ->
细节优化 ->
状态管理 ->
海量数据源,长列表渲染
方向分别加以探讨。

一 不能输在起跑线上,优化babel配置,webpack配置为项

1 真实项目中痛点

当我们用create-react-app或者webpack构建react工程的时候,有没有想过一个问题,我们的配置能否让我们的项目更快的构建速度,更小的项目体积,更简洁清晰的项目结构。
随着我们的项目越做越大,项目依赖越来越多,项目结构越来越来复杂,项目体积就会越来越大,构建时间越来越长,久而久之就会成了一个又大又重的项目,所以说我们要学会适当的为项目‘减负’,让项目不能输在起跑线上。

2 一个老项目

拿我们之前接触过的一个react老项目为例。我们没有用dva,umi快速搭建react,而是用react老版本脚手架构建的,这对这种老的react项目,上述的问题都会存在,下面让我们一起来看看。

我们首先看一下项目结构。

再看看构建时间。

为了方便大家看构建时间,我简单写了一个webpack,plugin ConsolePlugin ,记录了webpack在一次compilation所用的时间。

const chalk = require('chalk') /* console 颜色 */
var slog = require('single-line-log'); /* 单行打印 console */

class ConsolePlugin {
   
   
    constructor(options){
   
   
       this.options = options
    }
    apply(compiler){
   
   
        /**
         * Monitor file change 记录当前改动文件
         */
        compiler.hooks.watchRun.tap('ConsolePlugin', (watching) => {
   
   
            const changeFiles = watching.watchFileSystem.watcher.mtimes
            for(let file in changeFiles){
   
   
                console.log(chalk.green('当前改动文件:'+ file))
            }
        })
        /**
         *  before a new compilation is created. 开始 compilation 编译 。
         */
        compiler.hooks.compile.tap('ConsolePlugin',()=>{
   
   
            this.beginCompile()
        })
        /**
         * Executed when the compilation has completed. 一次 compilation 完成。
         */
        compiler.hooks.done.tap('ConsolePlugin',()=>{
   
   
            this.timer && clearInterval( this.timer )
            const endTime =  new Date().getTime()
            const time = (endTime - this.starTime) / 1000
            console.log( chalk.yellow(' 编译完成') )
            console.log( chalk.yellow('编译用时:' + time + '秒' ) )
        })
    }
    beginCompile(){
   
   
       const lineSlog = slog.stdout
       let text  = '开始编译:'
       /* 记录开始时间 */
       this.starTime =  new Date().getTime()
       this.timer = setInterval(()=>{
   
   
          text +=  '█'
          lineSlog( chalk.green(text))
       },50)
    }
}

构建时间如下:

打包后的体积:

3 翻新老项目

针对上面这个react老项目,我们开始针对性的优化。由于本文主要讲的是react,所以我们不把太多篇幅给webpack优化上。

① include 或 exclude 限制 loader 范围。

{
   
   
    test: /\.jsx?$/,
    exclude: /node_modules/,
    include: path.resolve(__dirname, '../src'),
    use:['happypack/loader?id=babel']
    // loader: 'babel-loader'
}

② happypack多进程编译

除了上述改动之外,在plugin中

/* 多线程编译 */
new HappyPack({
   
   
    id:'babel',
    loaders:['babel-loader?cacheDirectory=true']
})

③缓存babel编译过的文件

loaders:['babel-loader?cacheDirectory=true']

④tree Shaking 删除冗余代码

⑤按需加载,按需引入。

优化后项目结构

优化构建时间如下:

一次 compilation 时间 从23秒优化到了4.89秒

优化打包后的体积:

由此可见,如果我们的react是自己徒手搭建的,一些优化技巧显得格外重要。

关于类似antd UI库的瘦身思考

我们在做react项目的时候,会用到antd之类的ui库,值得思考的一件事是,如果我们只是用到了antd中的个别组件,比如<Button />,就要把整个样式库引进来,打包就会发现,体积因为引入了整个样式大了很多。我们可以通过.babelrc实现按需引入

瘦身前

.babelrc 增加对 antd 样式按需引入。

["import", {
   
   
    "libraryName":
    "antd",
    "libraryDirectory": "es",
    "style": true
}]

瘦身后

总结

如果想要优化react项目,从构建开始是必不可少的。我们要重视从构建到打包上线的每一个环节。

二 路由懒加载,路由监听器

react路由懒加载,是笔者看完dva源码中的 dynamic异步加载组件总结出来的,针对大型项目有很多页面,在配置路由的时候,如果没有对路由进行处理,一次性会加载大量路由,这对页面初始化很不友好,会延长页面初始化时间,所以我们想这用asyncRouter来按需加载页面路由。

传统路由

如果我们没有用umi等框架,需要手动配置路由的时候,也许路由会这样配置。

<Switch>
    <Route path={
   
   '/index'} component={
   
   Index} ></Route>
    <Route path={
   
   '/list'} component={
   
   List} ></Route>
    <Route path={
   
   '/detail'} component={
   
    Detail } ></Route>
    <Redirect from='/*' to='/index' />
</Switch>

或者用list保存路由信息,方便在进行路由拦截,或者配置路由菜单等。

const router = [
    {
   
   
        'path': '/index',
        'component': Index
    },
    {
   
   
        'path': '/list'',
        'component': List
    },
    {
   
   
        'path': '/detail',
        'component': Detail
    },
]

asyncRouter懒加载路由,并实现路由监听

我们今天讲的这种react路由懒加载是基于import 函数路由懒加载, 众所周知 ,import 执行会返回一个Promise作为异步加载的手段。我们可以利用这点来实现react异步加载路由

好的一言不合上代码。。。

代码

const routerObserveQueue = [] /* 存放路由卫视钩子 */
/* 懒加载路由卫士钩子 */
export const RouterHooks = {
   
   
  /* 路由组件加载之前 */
  beforeRouterComponentLoad: function(callback) {
   
   
    routerObserveQueue.push({
   
   
      type: 'before',
      callback
    })
  },
  /* 路由组件加载之后 */
  afterRouterComponentDidLoaded(callback) {
   
   
    routerObserveQueue.push({
   
   
      type: 'after',
      callback
    })
  }
}
/* 路由懒加载HOC */
export default function AsyncRouter(loadRouter) {
   
   
  return class Content extends React.Component {
   
   
    constructor(props) {
   
   
      super(props)
      /* 触发每个路由加载之前钩子函数 */
      this.dispatchRouterQueue('before')
    }
    state = {
   
   Component: null}
    dispatchRouterQueue(type) {
   
   
      const {
   
   history} = this.props
      routerObserveQueue.forEach(item => {
   
   
        if (item.type === type) item.callback(history)
      })
    }
    componentDidMount() {
   
   
      if (this.state.Component) return
      loadRouter()
        .then(module => module.default)
        .then(Component => this.setState({
   
   Component},
          () => {
   
   
            /* 触发每个路由加载之后钩子函数 */
            this.dispatchRouterQueue('after')
          }))
    }
    render() {
   
   
      const {
   
   Component} = this.state
      return Component ? <Component {
   
   
      ...this.props
      }
      /> : null
    }
  }
}

asyncRouter实际就是一个高级组件,将()=>import()作为加载函数传进来,然后当外部Route加载当前组件的时候,在componentDidMount生命周期函数,加载真实的组件,并渲染组件,我们还可以写针对路由懒加载状态定制属于自己的路由监听器beforeRouterComponentLoadafterRouterComponentDidLoaded,类似vuewatch $route 功能。接下来我们看看如何使用。

使用

import AsyncRouter ,{
   
    RouterHooks }  from './asyncRouter.js'
const {
   
    beforeRouterComponentLoad} = RouterHooks
const Index = AsyncRouter(()=>import('../src/page/home/index'))
const List = AsyncRouter(()=>import('../src/page/list'))
const Detail = AsyncRouter(()=>import('../src/page/detail'))
const index = () => {
   
   
  useEffect(()=>{
   
   
    /* 增加监听函数 */  
    beforeRouterComponentLoad((history)=>{
   
   
      console.log('当前激活的路由是',history.location.pathname)
    })
  },[])
  return <div >
    <div >
      <Router  >
      <Meuns/>
      <Switch>
          <Route path={
   
   '/index'} component={
   
   Index} ></Route>
          <Route path={
   
   '/list'} component={
   
   List} ></Route>
          <Route path={
   
   '/detail'} component={
   
    Detail } ></Route>
          <Redirect from='/*' to='/index' />
       </Switch>
      </Router>
    </div>
  </div>
}

效果

这样一来,我们既做到了路由的懒加载,又弥补了react-router没有监听当前路由变化的监听函数的缺陷。

三 受控性组件颗粒化 ,独立请求服务渲染单元

可控性组件颗粒化,独立请求服务渲染单元是笔者在实际工作总结出来的经验。目的就是避免因自身的渲染更新或是副作用带来的全局重新渲染。

1 颗粒化控制可控性组件

可控性组件和非可控性的区别就是dom元素值是否与受到react数据状态state控制。一旦由react的state控制数据状态,比如input输入框的值,就会造成这样一个场景,为了使input值实时变化,会不断setState,就会不断触发render函数,如果父组件内容简单还好,如果父组件比较复杂,会造成牵一发动全身,如果其他的子组件中componentWillReceiveProps这种带有副作用的钩子,那么引发的蝴蝶效应不敢想象。比如如下demo

class index extends React.Component<any,any>{
   
   
    constructor(props){
   
   
        super(props)
        this.state={
   
   
            inputValue:''
        }
    }
    handerChange=(e)=> this.setState({
   
    inputValue:e.target.value  })
    render(){
   
   
        const {
   
    inputValue } = this.state
        return <div>
            {
   
    /*  我们增加三个子组件 */ }
            <ComA />
            <ComB />
            <ComC />
            <div className="box" >
                <Input  value={
   
   inputValue}  onChange={
   
    (e)=> this.handerChange(e) } />
            </div>
            {
   
   /* 我们首先来一个列表循环 */}
            {
   
   
                new Array(10).fill(0).map((item,index)=>{
   
   
                    console.log('列表循环了' )
                    return <div key={
   
   index} >{
   
   item}</div>
                })
            }
            {
   
   
              /* 这里可能是更复杂的结构 */
              /* ------------------ */
            }
        </div>
    }
}

组件A

function index(){
   
   
    console.log('组件A渲染')
   return <div>我是组件A</div>
}

组件B,有一个componentWillReceiveProps钩子

class Index extends React.Component{
   
   
    constructor(props){
   
   
        super(props)
    }
    componentWillReceiveProps(){
   
   
        console.log('componentWillReceiveProps执行')
        /* 可能做一些骚操作 wu lian */
    }
    render(){
   
   
        console.log('组件B渲染')
        return <div>
            我是组件B
        </div>
    }
}

组件C有一个列表循环

class Index extends React.Component{
   
   
    constructor(props){
   
   
        super(props)
    }

    render(){
   
   
        console.log('组件c渲染')
        return <div>
              我是组件c
             {
   
   
                new Array(10).fill(0).map((item,index)=>{
   
   
                    console.log('组件C列表循环了' )
                    return <div key={
   
   index} >{
   
   item}</div>
                })
            }
        </div>
    }
}

效果

当我们在input输入内容的时候。就会造成如上的现象,所有的不该重新更新的地方,全部重新执行了一遍,这无疑是巨大的性能损耗。这个一个setState触发带来的一股巨大的由此组件到子组件可能更深的更新流,带来的副作用是不可估量的。所以我们可以思考一下,是否将这种受控性组件颗粒化,让自己更新 -> 渲染过程由自身调度。

说干就干,我们对上面的input表单单独颗粒化处理。

const ComponentInput = memo(function({
   
    notifyFatherChange }:any){
   
   
    const [ inputValue , setInputValue ] = useState('')
    const handerChange = useMemo(() => (e) => {
   
   
        setInputValue(e.target.value)
        notifyFatherChange && notifyFatherChange(e.target.value)
    },[])
    return <Input   value={
   
   inputValue} onChange={
   
    handerChange  }  />
})

此时的组件更新由组件单元自行控制,不需要父组件的更新,所以不需要父组件设置独立state保留状态。只需要绑定到this上即可。不是所有状态都应该放在组件的 state 中. 例如缓存数据。如果需要组件响应它的变动, 或者需要渲染到视图中的数据才应该放到 state 中。这样可以避免不必要的数据变动导致组件重新渲染.

class index extends React.Component<any,any>{
   
      
    formData :any = {
   
   }
    render(){
   
   
        return <div>
            {
   
    /*  我们增加三个子组件 */ }
            <ComA />
            <ComB />
            <ComC />
            <div className="box" >
               <ComponentInput notifyFatherChange={
   
    (value)=>{
   
    this.formData.inputValue = value } }  />
               <Button onClick={
   
   ()=> console.log(this.formData)} >打印数据</Button>
            </div>
            {
   
   /* 我们首先来一个列表循环 */}
            {
   
   
                new Array(10).fill(0).map((item,index)=>{
   
   
                    console.log('列表循环了' )
                    return <div key={
   
   index} >{
   
   item}</div>
                })
            }
            {
   
   
              /* 这里可能是更复杂的结构 */
              /* ------------------ */
            }
        </div>
    }
}

效果

这样除了当前组件外,其他地方没有收到任何渲染波动,达到了我们想要的目的。

2 建立独立的请求渲染单元

建立独立的请求渲染单元,直接理解就是,如果我们把页面,分为请求数据展示部分(通过调用后端接口,获取数据),和基础部分(不需要请求数据,已经直接写好的),对于一些逻辑交互不是很复杂的数据展示部分,我推荐用一种独立组件,独立请求数据,独立控制渲染的模式。至于为什么我们可以慢慢分析。

首先我们看一下传统的页面模式。

页面有三个展示区域分别,做了三次请求,触发了三次setState,渲染三次页面,即使用Promise.all等方法,但是也不保证接下来交互中,会有部分展示区重新拉取数据的可能。一旦有一个区域重新拉取数据,另外两个区域也会说、受到牵连,这种效应是不可避免的,即便react有很好的ddiff算法去调协相同的节点,但是比如长列表等情况,循环在所难免。

class Index extends React.Component{
   
   
    state :any={
   
   
        dataA:null,
        dataB:null,
        dataC:null
    }
    async componentDidMount(){
   
   
        /* 获取A区域数据 */
        const dataA = await getDataA()
        this.setState({
   
    dataA })
        /* 获取B区域数据 */
        const dataB = await getDataB()
        this.setState({
   
    dataB })
        /* 获取C区域数据 */
        const dataC = await getDataC()
        this.setState({
   
    dataC })
    }
    render(){
   
   
        const {
   
    dataA , dataB , dataC } = this.state
        console.log(dataA,dataB,dataC)
        return <div>
            <div> {
   
    /* 用 dataA 数据做展示渲染 */ } </div>
            <div> {
   
    /* 用 dataB 数据做展示渲染 */ } </div>
            <div> {
   
    /* 用 dataC 数据做展示渲染 */ } </div>
        </div>
    }
}

接下来我们,把每一部分抽取出来,形成独立的渲染单元,每个组件都独立数据请求到独立渲染。

function ComponentA(){
   
   
    const [ dataA, setDataA ] = useState(null)
    useEffect(()=>{
   
   
       getDataA().then(res=> setDataA(res.data)  )
    },[])
    return  <div> {
   
    /* 用 dataA 数据做展示渲染 */ } </div>
} 

function ComponentB(){
   
   
    const [ dataB, setDataB ] = useState(null)
    useEffect(()=>{
   
   
       getDataB().then(res=> setDataB(res.data)  )
    },[])
    return  <div> {
   
    /* 用 dataB 数据做展示渲染 */ } </div>
} 

function ComponentC(){
   
   
    const [ dataC, setDataC ] = useState(null)
    useEffect(()=>{
   
   
       getDataC().then(res=> setDataC(res.data)  )
    },[])
    return  <div> {
   
    /* 用 dataC 数据做展示渲染 */ } </div>
}  

function Index (){
   
   
    return <div>
        <ComponentA />
        <ComponentB />
        <ComponentC />
    </div>
}

这样一来,彼此的数据更新都不会相互影响。

总结

拆分需要单独调用后端接口的细小组件,建立独立的数据请求和渲染,这种依赖数据更新 -> 视图渲染的组件,能从整个体系中抽离出来 ,好处我总结有以下几个方面。

1 可以避免父组件的冗余渲染 ,react的数据驱动,依赖于 stateprops 的改变,改变state必然会对组件 render 函数调用,如果父组件中的子组件过于复杂,一个自组件的 state 改变,就会牵一发动全身,必然影响性能,所以如果把很多依赖请求的组件抽离出来,可以直接减少渲染次数。

2 可以优化组件自身性能,无论从class声明的有状态组件还是fun声明的无状态,都有一套自身优化机制,无论是用shouldupdate 还是用 hooksuseMemo useCallback ,都可以根据自身情况,定制符合场景的渲条
件,使得依赖数据请求组件形成自己一个小的,适合自身的渲染环境。

3 能够和redux ,以及redux衍生出来 redux-action , dva,更加契合的工作,用 connect 包裹的组件,就能通过制定好的契约,根据所需求的数据更新,而更新自身,而把这种模式用在这种小的,需要数据驱动的组件上,就会起到物尽其用的效果。

总结

在下一节我们将继续介绍 React 中的优化细节。

相关文章
|
6月前
|
前端开发 JavaScript UED
使用React Hooks优化前端应用性能
本文将深入探讨如何使用React Hooks来优化前端应用的性能,重点介绍Hooks在状态管理、副作用处理和组件逻辑复用方面的应用。通过本文的指导,读者将了解到如何利用React Hooks提升前端应用的响应速度和用户体验。
|
1月前
|
前端开发 JavaScript 算法
React 渲染优化策略
【10月更文挑战第6天】React 是一个高效的 JavaScript 库,用于构建用户界面。本文从基础概念出发,深入探讨了 React 渲染优化的常见问题及解决方法,包括不必要的渲染、大量子组件的渲染、高频事件处理和大量列表渲染等问题,并提供了代码示例,帮助开发者提升应用性能。
50 6
|
1月前
|
JSON 前端开发 JavaScript
【简单粗暴】如何使用 React 优化 AG 网格性能
【简单粗暴】如何使用 React 优化 AG 网格性能
28 3
|
6月前
|
监控 前端开发 API
如何优化React性能?
【4月更文挑战第9天】提升React应用性能的关键策略包括:使用React.memo和PureComponent防止不必要的重渲染;实施代码分割减少初始加载时间;借助React Profiler定位性能问题;优化state和props以减小大小和复杂性;设置列表项的key属性;避免内联函数和对象;自定义shouldComponentUpdate或React.memo比较函数;使用虚拟化技术渲染大量列表;通过Context API共享数据;以及利用服务端渲染加速首屏加载。不断监控和调整是优化的核心。
61 9
|
3月前
|
前端开发 JavaScript 大数据
React与Web Workers:开启前端多线程时代的钥匙——深入探索计算密集型任务的优化策略与最佳实践
【8月更文挑战第31天】随着Web应用复杂性的提升,单线程JavaScript已难以胜任高计算量任务。Web Workers通过多线程编程解决了这一问题,使耗时任务独立运行而不阻塞主线程。结合React的组件化与虚拟DOM优势,可将大数据处理等任务交由Web Workers完成,确保UI流畅。最佳实践包括定义清晰接口、加强错误处理及合理评估任务特性。这一结合不仅提升了用户体验,更为前端开发带来多线程时代的全新可能。
69 1
|
3月前
|
前端开发 UED 开发者
React组件优化全攻略:深度解析让你的前端应用飞速运行的秘诀——从PureComponent到React.memo的彻底性能比较
【8月更文挑战第31天】在构建现代Web应用时,性能是提升用户体验的关键因素。React作为主流前端库,其组件优化尤为重要。本文深入探讨了React组件优化策略,包括使用`PureComponent`、`React.memo`及避免不必要的渲染等方法,帮助开发者显著提升应用性能。通过实践案例对比优化前后效果,不仅提高了页面渲染速度,还增强了用户体验。优化React组件是每个开发者必须关注的重点。
64 0
|
3月前
|
前端开发 UED 开发者
React.lazy()与Suspense:实现按需加载的动态组件——深入理解代码分割、提升首屏速度和优化用户体验的关键技术
【8月更文挑战第31天】在现代Web应用中,性能优化至关重要,特别是减少首屏加载时间和提升用户交互体验。React.lazy()和Suspense组件提供了一种优雅的解决方案,允许按需加载组件,仅在需要渲染时加载相应代码块,从而加快页面展示速度。Suspense组件在组件加载期间显示备选内容,确保了平滑的加载过渡。
134 0
|
3月前
|
缓存 前端开发 JavaScript
深入探索优化React应用程序的策略
【8月更文挑战第20天】
43 0
|
4月前
|
存储 前端开发 JavaScript
react hooks 学习进阶
【7月更文挑战第12天】 React Hooks(自16.8版起)让函数组件能处理状态和副作用。useState用于添加状态管理,useEffect处理副作用,useContext共享数据,useReducer处理复杂状态逻辑,useRef获取引用。进阶技巧涉及性能优化,如useMemo和useCallback,以及遵循规则避免在不适当位置调用Hooks。理解异步更新机制和结合Redux等库提升应用复杂性管理。持续学习新技巧是关键。
52 0
|
5月前
|
前端开发 JavaScript 开发者
React和TypeScript各自以其独特的优势赢得了广大开发者的青睐
【6月更文挑战第12天】React和TypeScript是前端开发的强强联合。TypeScript提供静态类型检查和面向对象特性,增强代码健壮性和团队协作效率;React凭借组件化、高性能和丰富生态系统引领UI构建。两者结合,能精确定义React组件类型,提升代码组织和维护性,通过安装TypeScript、配置、编写及构建步骤,可在React项目中实现这一优势。这种结合为前端开发带来进步,未来应用将更加广泛。
55 1