天天看點

React 渲染性能優化

性能優化

在React内部已經使用了許多巧妙的技術來最小化由于Dom變更導緻UI渲染所耗費的時間。對于很多應用來說,使用React後無需太多工作就會讓用戶端執行性能有質的提升。然而,還是很其他更多的辦法來加速React程式。

使用生産模式來建構應用

如果在開發和使用的過程中感覺了React應用有明顯的性能問題,請先确認是否已經建構了壓縮後的生産包:

  • 在單頁面用中,打包之後的生産檔案應該是.min.js版本。
  • 對于Brunch(html打包工具:http://brunch.io/),打包指令需要包含-p标記。
  • 對于Browserify(UMD規範打包工具:http://browserify.org/),打包時需要增加生産配置參數—— 

    NODE_ENV=production

  • 對于在建立React App時,需要執行 

    npm run build

     指令,并按照說明操作。
  • 對于Rollup(JavaScript代碼高效壓縮工具:https://rollupjs.org/),生産打包時需要在  commonjs  插件之前使用  replace  插件:
    plugins: [
      require('rollup-plugin-replace')({
        'process.env.NODE_ENV': JSON.stringify('production')
      }),
      require('rollup-plugin-commonjs')(),
      // ...
    ]           
    可以在這裡看到 一個完整的例子: see this gist
  • 使用Webpack打包,需要在打生産包的配置腳本中增加以下配置和插件:
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    new webpack.optimize.UglifyJsPlugin()           

切記不要将開發模式的包釋出到生産環境,因為開發包中額外包含了許多用于輔助的測試的資訊,無論在加載還是執行時,它都比較慢。

使用chrome分析元件的渲染時間線

在開發模式中下你可以直接在chrome的性能工具中看到元件是如何裝載、更新和解除安裝的。例如下面的圖檔展示的效果:

在chrome中按照以下步驟執行:

  1. 使用?react_perf作為url參數(例如:http://localhost:3000/?react_perf)
  2. 打開chrome的開發工具Timeline,然後點選Record(左上角的紅色按鈕)。
  3. 執行你要監控的操作。請不要記錄超過20秒,這可能會導緻chrome假死。
  4. 停止記錄。
  5. React事件将會批量記錄在User Timing标簽裡。

關于分析的資料,需要明确的是:渲染的時間隻是一個相對的參考值,在建構成生産包之後,渲染的速度會更快。盡管如此,這些資料仍然能夠幫助我們分析是否有不相關的UI被錯誤的更新,以及UI更新的頻率和深度。

目前隻有Chrome、Edge和IE支援這個特性,但是官方正在使用

User Timing API 标準

 讓更多浏覽器支援這個特性。

手工避免重複渲染

React建構和維護了一個内部的虛拟Dom,這個Dom和真實的UI是互相映射的關系,他包含從使用者自定義元件中傳回的各種React元素。這個虛拟的Dom使得React可以避免重複渲染相同的Dom節點并在通路存在的節點時直接使用React的虛拟層資料,這樣設計的原因是重複渲染浏覽器或web view的UI比操作一個JavaScript的對象要慢許多。在React Native也采用同樣的處理方式。

當元件的props和state變更時,React會将最新傳回的元素與之前舊的元素進行對比來确定是否真的需要重新渲染真實的Dom。當他們不相等時,React會更新真實的Dom。

在某些情況下,可以在自定義元件中重載

shouldComponentUpdate

方法來加速觸發渲染的比對的過程。該方法的預設實作傳回參數為true,此時React将按照原來的方式進行比對和渲染:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}           

如果在某些情況下能夠清晰的明确元件不需要重新渲染,可以在 

shouldComponentUpdate

 方法中傳回 false,這樣會讓讓元件跳過整個渲染過程,包括不再調用目前元件和子元件的render()方法。

shouldComponentUpdate 的執行過程

下面是一個元件結構樹。圖中,“SCU”表示 

shouldComponentUpdate

 方法傳回的值(綠色true,紅色fasle),“vDOMEq”表示React的比對是否一緻(綠色true,紅色fasle),有顔色的紅圈表示是否執行了UI重繪(綠色表示沒重繪,紅色表示執行重繪)。

React 渲染性能優化

在C2元件中,

shouldComponentUpdate

 方法傳回了false,是以React不會判斷是否需要重新渲染C2并且不執行render()方法, 是以在C4和C5中不再執行

shouldComponentUpdate

 方法。

對于C1和C3,

shouldComponentUpdate

 都傳回了true,是以React必須對着2個元件進行比對。對于C6,

shouldComponentUpdate

 傳回true,而且比對的結果是需要UI重繪,是以C6會更新他們的真實Dom。

還有一個值得關心的元件是C8,React在這個元件中執行了render()方法,但是由于虛拟Dom并沒有發生變更,前後比對一緻,是以并沒有發生真實Dom渲染。

在整個過程中React僅僅變更了C6元件的UI樣式,C8由于前後虛拟Dom一緻是以沒有真正的執行UI渲染。C2、C2的子元件以及C7沒有執行render()方法。

一個shouldComponentUpdate的例子

在例子中,當props.color和state.count發生變更時進行UI渲染,我們在 

shouldComponentUpdate

 方法中進行檢查:

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    //隻判斷props.color和nextState.count是否變更,其他情況均不渲染
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}           

在這段代碼中,

shouldComponentUpdate

 僅僅檢查 

props.color

和 

state.count

是否發生變更,如果他們的值沒有修改,元件将不會發生任何更新。在實際使用中,元件往往比這個複雜,我們可以使用類似于“淺比較”(關于淺比較可以參看: 

Shallow Compare

)的模式來比對所有的屬性或狀态是否發生變更。React提供了這個模式的一個實作元件,隻要讓元件繼承自 

React.PureComponent

即可。我們可以将代碼進行下面的修改:

//繼承自React.PureComponent
class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}           

在大部分情況下,隻要使用 

React.PureComponent

 就可以代替我們自己重載 

shouldComponentUpdate

方法,但是它僅僅适用于“淺比較”,是以這個元件不适用于props和state資料發生突變的情況。

附:資料突變(mutated)是指變量的引用沒有改變(指針位址未改變),但是引用指向的資料發生了變化(指針指向的資料發生變更)。例如const x = {foo:'foo'}。x.foo='none' 就是一個突變。

在更複雜的資料結構中還會存在一些問題。例如下面的代碼,我們希望

ListOfWords

 元件将words參數渲染成一個逗号分隔的字元串,而父元件監控點選事件,每次點選都會增加一個單詞到清單中,但是下面的代碼并不會正确工作:

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 這段内容會導緻代碼不按照預期工作。
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}           

導緻代碼無法正常工作的原因是 

PureComponent

 僅僅對 this.props.words的新舊值進行“淺比較”。在words值在

handleClick

中被修改之後,即使有新的單詞被添加到數組中,但是this.props.words的新舊值在進行比較時是一樣的(引用對象比較),是以 

ListOfWords

 一直不會發生渲染。

非突變資料的價值

有一個簡單的方法預防上面提到的問題,就是在使用prop和state時防止資料發生突變。例如下面的例如,我們用數組的concat方法來代替等号“=”,這樣在concat後會産生一個新的數組指派給this.state.words:

handleClick() {
  this.setState(prevState => ({
    words: prevState.words.concat(['marklar'])
  }));
}           

ES6支援清單擴充文法,是以我們更容易在es6中實作非突變的資料指派,例如:

handleClick() {
  this.setState(prevState => ({
    words: [...prevState.words, 'marklar'],
  }));
};           

可以重寫傳統的指派語句防止對象中的資料發生資料突變。下面的例子有一個名為 

colormap

 的對象,我們想在修改 

colormap.right

 的值時渲染元件,我們可以這樣重寫元件:

function updateColorMap(colormap) {
  colormap.right = 'blue'; //淺拷貝,指針位址未變,資料發生變化。
}           

可以使用 

Object.assign

 方法來防止資料突變:

function updateColorMap(colormap) {
  // 深拷貝,修改傳回對象的位址
  return Object.assign({}, colormap, {right: 'blue'});
}           

修改後 

updateColorMap

 方法傳回一個新的執行個體。需要注意的是某些浏覽器不支援 

Object.assign

方法,我們需要使用polyfill(差異化抹平,比如我們引入了babel-polyfill)來解決這個問題。

有一個新的JavaScript方案是使用 擴充傳播特性(見 

object spread properties

 )來解決資料突變問題,實作如下:

function updateColorMap(colormap) {
  return {...colormap, right: 'blue'};
}           

如果是建構React的App應用,那麼以上方法都能夠很好的支援,如果是在浏覽器環境使用,需要引入polyfill機制。

使用不可變的資料結構

Immutable.js

 是解決資料突變問題的另外一種解決方案。它提供不可變、持久化的集合。集合包含下列結構:

  • Immutable:一旦資料被建立,改集合不能在任何其他地方修改。
  • Persistent:可以從已有的的資料集合(例如set)來建立新的資料集合。在建立新的資料集合後,已有的資料集合依然有效。
  • 結構分享(Structural Sharing):使用和原始資料盡可能相似的結建構立新的資料集合,并将複制降至最低,盡可能的提高效率。

資料結構不可變的特性使跟蹤資料變化變得很簡單。任何變更将始終導緻建立一個新的對象,是以我們隻需要檢查引用(指針位址)是否已經被修改即可确定資料是否已經修改。例如在正常的JavaScript代碼中:

const x = { foo: "bar" };
const y = x;
y.foo = "baz";
x === y; // true           

盡管y的值已經被修改,但是它和x都是同一個引用(指向相同的位址),是以最後的比較語句會傳回true。我們可以使用 immutable.js來修改代碼:

const SomeRecord = Immutable.Record({ foo: null});
const x = new SomeRecord({ foo: 'bar'});
const y = x.set('foo', 'baz');
x === y; // false           

在這個例子中,由于x突變時使用了新的引用,我們可以安全的假設x已經發生改變。

還有兩個庫可以幫我們建構不可變資料: 

seamless-immutable

 and 

immutability-helper

不可變的資料結構為我們跟蹤資料對象變更提供了更加簡便的方式,這是我們快速實作

shouldComponentUpdate

方法的基礎。使用不可變資料後,可以為React提供不錯的性能提升。

繼續閱讀