Vue轉React兩個月來總結的性能優化方法

前言

換了新公司,工做中使用的技術棧也從Vue換到了React,做爲一個React新人,常常的總結和思考才能更快更好的瞭解這個框架。這裏分享一下我這兩個月來使用React總結的一些性能優化的方法。css

由於目前公司的項目是全面擁抱hooks的,因此只會涉及function組件寫法,不包含class組件寫法的相關內容。注意:本文只涉及到一些業務開發層面的代碼優化,不少通用的優化思想,好比虛擬列表,圖片懶加載,節流防抖,webpack優化等等內容都不會涉及到。html

React的更新機制

要來優化代碼,首先咱們來簡單瞭解一下React的更新機制。看下圖react

咱們重點來看第一步到第二步這個過程,當一個組件的propsstate改變的時候,當前組件會從新render。當前組件下面的全部子、孫、曾孫。。。組件不論是否用到了父組件的props,全都會從新render。這是一個跟Vue更新原理很大的區別,Vue中全部組件的渲染都是由各自的data控制,父組件跟子組件的渲染都是獨立的。webpack

本文關於React的性能優化,主要是三塊內容,web

  • 提升diff算法的dom重複利用率
  • 減小資源的加載
  • 減小組件的render次數和計算量(最重點的一塊)

遍歷列表使用key

這個跟Reactdiff算法有關,是一個很簡單,能夠做爲必須遵照規範的一個優化。算法

在全部的須要遍歷的列表當中,都加上一個key值,這個值不能是那種遍歷時候的序號,必須是一個固定值。好比該條數據id緩存

這個key能夠幫助diff算法更好的複用dom元素,而不是銷燬再從新生成。性能優化

精簡沒必要要的節點

由於Reactdiff算法跟Vue同樣是對於虛擬dom從父到子,一層層同級的比較。因此減小節點的嵌套,能夠有效的減小diff算法的計算量。markdown

<div className="root">
  <div>
    <h1>個人名字:{name}</h1>
  </div>
  <div>
    <p>個人簡介: {content}</p>
  </div>
</div>
// 徹底能夠精簡爲
<div className="root">
  <h1>個人名字:{name}</h1>
  <p>個人簡介: {content}</p>
</div>
複製代碼

精簡state

不須要把全部狀態都放在組件的state中,只有那些須要響應式的數據才應該存入state框架

不要使用CSS內聯樣式

React中處理樣式有三種

  • css Module
  • css in js(以styled-components爲表明的)
  • 內聯css (把樣式寫在組件的style裏)

對於css Modulecss in js來講,其實都有優缺點,用哪一個其實都沒問題。雖然不少人說css Module性能要比css in js好,可是那點性能真的不值一提。

這邊要說的是內聯css,若是你沒有那種必須經過控制style來修改組件內容或者樣式的需求的話,千萬不要寫。

這塊在後面render的優化中會細講。

使用useMemo減小重複計算

來看一個例子

import React from 'react';

export default function App() {
  const [num, setNum] = useState(0);
  
  const [factorializeNum, setFactorializeNum] = useState(5);

  // 階乘函數
  const factorialize = (): Number => {
    console.log('觸發了');
    let result = 1;
    for (let i = 1; i <= factorializeNum; i++) {
      result *= i;
    }
    return result;
  };

  return (
    <> {num} <button onClick={() => setNum(num + factorialize())}>修改num</button> <button onClick={() => setFactorializeNum(factorializeNum + 1)}>修改階乘參數</button> </>
  );
}
複製代碼

在這個組件裏,每次點擊修改num這個按鈕,都會打印一次觸發了,階乘函數會從新計算一遍。可是其實參數是沒有變化的,返回的結果也是沒有變化的。

咱們可使用useMemo來緩存計算結果,避免重複計算。

import React, { useMemo } from 'react';

export default function App() {
  const [num, setNum] = useState(0);
  
  const [factorializeNum, setFactorializeNum] = useState(5);

  // 當factorializeNum值不變的時候,這個函數不會再重複觸發了
  const factorialize = useMemo((): Number => {
    console.log('觸發了');
    let result = 1;
    for (let i = 1; i <= factorializeNum; i++) {
      result *= i;
    }
    return result;
  }, [factorializeNum]);

  return (
    <> {num} <button onClick={() => setNum(num + factorialize())}>修改num</button> <button onClick={() => setFactorializeNum(factorializeNum + 1)}>修改階乘參數</button> </>
  );
}
複製代碼

多用&&或者三元表達式

咱們寫一些組件的時候常常會碰到這種需求,根據參數的不一樣,渲染不一樣的組件。例

const App = () => {
  const [type, setType] = useState(1);

  if (type === 1) {
    return (
      <> <Component1>component1</Component1> <Component2>component2</Component2> <Component3>component3</Component3> </>
    );
  }

  return (
    <Component2>component2</Component2>
    <Component3>component3</Component3>
  );
};
複製代碼

上面的代碼乍一看其實沒啥問題,根據類型的不一樣,返回不一樣的組件。可是對於diff算法來講,它會對同級的新舊節點進行比較,當類型變化的時候,Component1沒有生成了,對於diff算法來講,他會拿舊的第一項Component1跟新的第一項Component2比較,由於沒有key,並且這是組件, diff算法會深刻到組件的子元素中再去同級比較。假設這三個組件都是不同的,diff算法就會把舊節點的三個組件所有銷燬,再從新生成兩個新組件。

可是按性能來講,其實只須要銷燬第一個組件,複用剩下的那兩個就能夠。

key固然能夠,可是咱們可使用更簡單的方式。

<>
  {type === 1 && <Component1>component1</Component1>}
  <Component2>component2</Component2>
  <Component3>component3</Component3>
</>
複製代碼

當類型不符合的時候,·component1的位置會放置一個nulldiff算法會拿這個null跟舊的component1進行比較,剩下的兩個組件順序不變,diff算法會進行復用。並且這種方式,代碼也更加精簡。

異步組件(懶加載組件)

最典型場景是tab頁面切換,當tab切換到相應的頁面上時,再去加載相應頁面的組件js。

這些的組件資源不會包含在主包裏,在後續在用戶須要的時候,再去加載相關的組件js資源。能夠提升頁面的加載速度,減小無效資源的加載。

主要用到兩個方法React.SuspenseReact.lazy

import React from 'react';

export default (props) => {
  return (
    <> <Drawer> <Tabs defaultActiveKey="1"> <TabPane> <React.Suspense fallback={<Loading />}> {React.lazy(() => import('./Component1'))} </React.Suspense> </TabPane> <TabPane> <React.Suspense fallback={<Loading />}> {React.lazy(() => import('./Component2'))} </React.Suspense> </TabPane> </Tabs> </Drawer> </>
  );
};
複製代碼

使用上面的方法以後,webpack會把這個import的組件單獨打包成一個js。在tab切換到相應的頁面時,加載這個js,渲染出相應的組件。

減小組件的render(重點)

使用React.memo

咱們先來看個例子

import React from 'react';

const Child = () => {
  console.log('觸發Child組件渲染');
  return (
    <h1>這是child組件的渲染內容!</h1>
  )
};

export default () => {
  const [num, setNum] = useState(0);
  
  return (
    <> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child /> </>
  );
}
複製代碼

當咱們每次點擊num加1這個按鈕的時候,咱們都會在控制檯發現打印了一次觸發Child組件渲染。說明Child這個組件在咱們父組件的state變化以後,每次都會從新render

咱們可使用React.memo來避免子組件的重複render

import React from 'react';

const Child = React.memo(() => {
  console.log('觸發Child組件渲染');
  return (
    <h1>這是child組件的渲染內容!</h1>
  )
});

export default () => {
  const [num, setNum] = useState(0);
  
  return (
    <> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child /> </>
  );
}
複製代碼

React.memo會判斷子組件的props是否有改變,若是沒有,將不會重複render。這時候咱們點擊num加1按鈕,Child將不會重複渲染。

不要直接使用內聯對象

咱們再來看一個例子

import React from 'react';

const Child = React.memo((props) => {
  const { style } = props;
  console.log('觸發Child組件渲染');
  return (
    <h1 style={style}>這是child組件的渲染內容!</h1>
  )
});

export default () => {
  const [num, setNum] = useState(0);
  
  return (
    <> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child style={{color: 'green'}}/> </>
  );
}
複製代碼

這個相比較上一個例子,就是給Child組件多傳入了一個style參數。傳入的參數是一個靜態的對象,你以爲如今子組件會重複渲染嗎?

一開始我以爲不會,實際測試下來,發現子組件又開始了重複渲染。

state改變,父組件從新render的時候,像這種{color: 'green'}會從新生成,這個對象的內存地址會變成一個新的。而React.memo只會對props進行淺層的比較,由於傳入對象的內存地址修改了,因此React.memo就覺得傳入的props有新的修改,就從新渲染了子組件。

咱們能夠有兩種方式來修改。

// 若是傳入的參數是徹底獨立的,沒有任何的耦合
// 能夠將該參數,提取到渲染函數以外
const childStyle = { color: 'green' };
export default () => {
  const [num, setNum] = useState(0);
  
  return (
    <> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child style={childStyle}/> </>
  );
}
// 若是傳入的參數須要使用渲染函數裏的參數或者方法
// 可使用useMemo
export default () => {
  const [num, setNum] = useState(0);
  const [style, setStyle] = useState('green');
  // 若是不須要參數
  const childStyle = useMemo(() => ({ color: 'green' }), []);
  // 若是須要使用state或者方法
  const childStyle = useMemo(() => ({ color: style }), [style]);
  return (
    <> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child style={childStyle}/> </>
  );
}
複製代碼

傳入組件的函數使用React.useCallback

函數致使子組件從新渲染的原理跟上面的內聯對象同樣,也是由於父組件的從新渲染,致使函數方法的內存地址發生變化,因此React.memo會認爲props有變化,致使子組件重複渲染。

咱們可使用React.useCallback來緩存函數方法,避免子組件的重複渲染。

export default () => {
  const [num, setNum] = useState(0);
  const oneFnc = useCallback(() => {
    console.log('這是傳入child的方法');
  }, []);
  return (
    <> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child onFnc={oneFnc} /> </>
  );
}
複製代碼

同理,要避免在子組件的傳入參數上直接寫匿名函數。

// 不要直接寫匿名函數
<Child onFnc={() => console.log('這是傳入child的方法')} />
複製代碼

使用children來避免React Context子組件的重複渲染

對於咱們經常使用的Context,咱們不但可使用React.Memo來避免子組件的重複渲染,咱們還能夠經過children的方式。

import React, { useContext, useState } from 'react';

const DemoContext = React.createContext();

const Child = () => {
  console.log('觸發Child組件渲染');
  return (
    <h1 style={style}>這是child組件的渲染內容!</h1>
  )
};

export default () => {
  const [num, setNum] = useState(0);
  return (
    <DemoContext.Provider value={num}> <button onClick={() => setNum(num + 1)}>num加1</button> <Child /> {...一些其餘須要使用num參數的組件} </DemoContext.Provider>
  );
}
複製代碼

在這裏可使用children方法來避免Child的重複渲染。

import React, { useContext, useState } from 'react';

const DemoContext = React.createContext();

const Child = () => {
  console.log('觸發Child組件渲染');
  return (
    <h1 style={style}>這是child組件的渲染內容!</h1>
  )
};

function DemoComponent(props) {
  const { children } = props;
  const [num, setNum] = useState(0);
  return (
    <DemoContext.Provider value={num}> <button onClick={() => setNum(num + 1)}>num加1</button> {children} </DemoContext.Provider>
  );
}

export default () => {
  return (
    <DemoComponent> <Child /> {...一些其餘須要使用num參數的組件} </DemoComponent>
  );
}
複製代碼

這時候,修改state,只是對於DemoComponent這個組件內部進行render,對於外部傳入的Child組件,將不會重複渲染。

總結

上面這些都是我平時開發當中真實碰到過的問題,相信也是全部React開發者都會碰到的問題,涉及到的技術不深,但願給一些新入坑React的同窗有所幫助。

感謝

謝謝你們的閱讀,若是以爲對你有所幫助,請幫忙點個贊支持一下!

相關文章
相關標籤/搜索