React項目性能優化


1. 使用生產版本和Fragment

1. 生產版本

確保發布的代碼是生產模式下(壓縮)打包的代碼。

一般運行npm run build命令。

直接從webpack看配置文件,需要設置mode = 'production'。 調用teaser-webpack-plugin

React Devtools可以根據地址欄右側圖標顏色判斷是否是生產模式。

2. Fragment

減少不必要節點的生成。也可以使用空標簽(<></>)

2. 類組件使用PureComponent

減少不必要的重復渲染!

PureComponent的原理是重新設置了shouldComponentUpdate(nextProps, nextState)方法。

在該方法中將nextProps和this.props對象淺比較,將nextState和this.state對象進行淺比較

PureComponent模擬代碼實現:

import React from 'react';

function shallowCompare(obj1, obj2) {
  if(obj1 === obj2) {
    return true;
  }
  if(typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }
  const obj1Keys = Object.keys(obj1);
  const obj2Keys = Object.keys(obj2);
  if (obj1Keys.length !== obj2Keys.length) {
    return false;
  }
  for(let key of obj1Keys) {
    // 如果obj1[key]和obj2[key]為對象;淺比較有些情況會失效
    if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
      return false;
    }
  }
  return true
}
export default class PureComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return !(shallowCompare(nextProps, this.props) && shallowCompare(nextState, this.state))
  }
}

測試用例:

/*******父組件********/
class App extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      number: 1
    }
    this.ref = React.createRef();
  }
  add = () => {
    // this.state.number通過prop傳遞給Counter
    // Counter通過PureComponnet進行淺比較
    // Number(this.ref.current.value)為0時,Counter不刷新
    this.setState(state => ({
      number: state.number + Number(this.ref.current.value)
    }))
  }
  render() {
    console.log('App Render');
    return (
      <div>
        <Counter number={this.state.number} />
        <input ref={this.ref} />
        <button onClick={this.add}>+</button>
      </div>
    )
  }
}

/*********子組件***********/
import React from 'react';
import PureComponent from './pureComponent';

export default class Counter extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    }
  }
  // 證明PureComponent會對this.state和nextState進行淺比較
  add = () => {
    // 不會導致render
    this.setState((state) => ({
      count: state.count + 0 // 0不會刷新,其他值會刷新
    }))
  }
  render() {
    console.log('Counter Render');
    return (
      <div>
        <div>
          Counter:{this.state.count}
        </div>
        <button onClick={this.add}>+</button>
        <div>
          {this.props.number}
        </div>        
      </div>
    )
  }
};

3. 不可變數據-immutable

上面的PureComponent只是對簡單數據(值為原始類型)進行淺比較;

而實際開發應用中,數據都是多層嵌套的復雜數據;只使用PureComponent可能會導致render失效。

示例(錯誤示例-數組):

  state= {words: []}; 
  add = () => {
    // 將一個對象賦值給另一個變量;兩者對應同一個地址
    let newWords = this.state.words;
    // push,pop,shift,splice方法都不會改變原址;
    newWords.push(this.ref.current.value);
    // 如果在子組件使用了PureComponent淺比較words的值,nextProps.words === this.props.words
    // 所以不會引起子組件Counter的render方法調用
    this.setState({ 
      words: newWords
    });
  }

上面的示例可以通過不可變數據修復BUG。

不可變數據: 保持原數據不變,生成一個新的對象進行賦值

示例1:

  add = () => {
    // concat方法調用后,返回一個新數組
    this.setState(state => ({
      words: state.words.concat(this.ref.current.value)
    }))
  }

示例2:

  add = () => {
    // []符號本身就是new Array()的簡寫形式
    this.setState(state => ({
      words: [...state.words, this.ref.current.value]
    }))
  }

上面的示例都是復合數據是數組的情形;對於數據是對象的示例如下:

錯誤示例(對象):

class App extends React.PureComponent{
  constructor(props) {
    super(props);
    this.state = {
      words: {}
    }
    this.ref = React.createRef();
  }
  add = () => {
    // Object.assign()被修改對象在第一個參數時,直接在原state.words對象地址上修改;
    // 對於PureComponent來說,nextState和this.state相同,永遠不會render
    this.setState(state => ({
      words: Object.assign(state.words, {a:this.ref.current.value})
    }))
  }
  render() {
    console.log('App Render');
    return (
      <div>
        <input ref={this.ref} />
        <button onClick={this.add}>+</button>
      </div>
    )
  }
}

使用不可變數據,生成新對象的方法修改;

示例1: 

  add = () => {
    // Object.assign()第一個參數是空對象,表明生成一個新的對象
    this.setState(state => ({
      words: Object.assign({}, state.words, {a:this.ref.current.value})
    }))
  }

示例2:

  add = () => {
    // {}本身是new Object的簡寫形式
    this.setState(state => ({
      words: {...state.words, a: this.ref.current.value}
    }))
  }

上面的方法雖然解決了使用PureComponent組件時,對象導致的不刷新問題;

但是,會出現,只要是調用setState就重新的render的情況,盡管對應的值其實是一樣的。

因為對於js引擎來說,任何兩個對象都不相同。而且,嵌套層次特別深時,書寫也復雜。

immutable庫可以解決這個!

immutable

immutable不僅可以生成新的對象;還可以對對象進行深層次的值比較。

import { Map, is } from 'immutable';

class App extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      // is方法判斷的時候,深層比較只能比較從immutable對象變化而來的對象
      // 因為這里考慮words會進行深層變化,要追蹤變化,需要將其變成immutable對象
      words: Map({}),
      number: 1
    }
    this.ref = React.createRef();
  }
  shouldComponentUpdate(nextProps, nextState) {
    // is方法用於比較兩個immutable對象是否值相等
    return !(is(Map(nextState), Map(this.state)) && is(Map(nextProps), Map(this.props)))
  }
  add = () => {
    // set方法給immutable對象賦值;
    // 另可以通過get獲取immutable對象的值
    // this.state.words.get('a')
    this.setState(state => ({
      words: state.words.set('a', this.ref.current.value)
    }))
  }
  reset = () => {
    this.setState(state => ({
      number: state.number + 1
    }))
  }
  render() {
    console.log('App Render');
    return (
      <div>
        <input ref={this.ref} />
        <button onClick={this.add}>+</button><br/>
        <button onClick={this.reset}>reset</button>
      </div>
    )
  }
}

4. 函數組件使用React.memo避免重復渲染

React.memo()本質上是將函數組件轉為繼承React.PureComponent的類組件的高階組件。

稍有不同的是,只比較props的值。

代碼實現如下:

function memo(WrappedComponent) {
  return class extends React.PureComponent {
    render() {
      return (
        <WrappedComponent {...this.props} />
      )
    }
  }
}

對於深層比較,還可以接受第二個參數

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 傳入 render 方法的返回結果與
  將 prevProps 傳入 render 方法的返回結果一致則返回 true,
  否則返回 false
  */
}
export default React.memo(MyComponent, areEqual);

5. React.lazy()和Suspense進行懶加載

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);

其中:React.lazy()中import只支持默認導出的組件。

其外要包含在Suspense組件中。

6. 異常捕獲邊界(Error Boundaries)

捕獲發生異常的React組件。將異常組件和正常組件分割開。

提高用戶的體驗性能。

import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

const MyComponent = () => (
  <div>
    <MyErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </MyErrorBoundary>
  </div>
);

MyErrorBoundary代碼:

class MyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能夠顯示降級后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同樣可以將錯誤日志上報給服務器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定義降級后的 UI 並渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

7. 骨架屏

骨架屏插件以react-content-loader(svg圖片生層)為基礎。

用於在頁面初始加載時,避免出現白屏現象。

代碼參考

8.長列表優化

虛擬化長列表:只加載可視范圍內的數據。

當網站需要加載大批量數據(成千上萬)時,會加載特別慢。

這個時候我們可以使用“虛擬滾動”技術(react-window或者react-virtualized),只渲染當前屏幕范圍內的數據。

鼠標滾動去觸發事件,再渲染一屏。

react-window用法示例:

import { FixedSizeList as List } from 'react-window';

const Row = ({index, style}) => (
  <div style={{...style, backgroundColor: getRandomColor()}}>
    Row{index}
  </div>
)
function Container(props) {
  return (
    <List
      height={200}
      itemCount={100}
      itemSize={30}
      width={'100%'}
    >
      {Row}
    </List>
  )
}
function getRandomColor() {
  const color = Math.floor((Math.random()*0xFFFFFF)).toString(16)
  if (color.length === 6) {
    return '#' + color
  }
  return getRandomColor();
}
ReactDOM.render(<Container />, window.root)

原理是監聽鼠標的滾動事件。

實現react-window的FixedSizeList的源碼如下:

import React from 'react';

export class FixedSizeList extends React.Component {
  constructor(props) {
    super(props);
    this.ref = React.createRef();
    this.state = {
      start: 0
    }
  }
  componentDidMount() {
    this.ref.current.addEventListener('scroll', () => {
      // 注意: 監聽函數內部有this.ref,需要使用箭頭函數
      let scrollTop = this.ref.current.scrollTop;
      this.setState({
        start: Math.floor(scrollTop/this.props.itemSize)
      })
    })
  }
  render() {
    const { itemSize, itemCount, height, width, } = this.props;
    const containerStyle = {
      height, width, position: 'relative', overflow: 'auto'
    }
    let children = [];
    const itemStyle = {
      height: itemSize,
      width: '100%',
      position: 'absolute',
      top: 0,
      left: 0
    }
    const { start } = this.state;
    for(let i=start; i< start + Math.ceil(height/itemSize); i++) {
      children.push(this.props.children({index: i, style: {...itemStyle, top: i*30}}))
    }
    return (
      <div style={containerStyle} ref={this.ref}>
        <div style={{width: '100%', height: itemSize*itemCount}}>
          {children}
        </div>
      </div>
    )
  }
}

9. 根據性能優化工具修改代碼

1. 使用Chrome的Performance工具

2. React Devtools的Profiler工具分析;

   通過React.Profiler組件包裹需要分析渲染時間的組件(不適合生產環境)。

10. SEO優化-預渲染

使用prerender-spa-plugin的puppeteer(無頭瀏覽器)功能。

11. 圖片懶加載

使用react-lazyload插件

12. key優化

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM