React進階—性能優化

React性能優化思路

軟件的性能優化思路就像生活中去看病,大體是這樣的:javascript

  1. 使用工具來分析性能瓶頸(找病根)html

  2. 嘗試使用優化技巧解決這些問題(服藥)java

  3. 使用工具測試性能是否確實有提高(療效確認)react

React性能優化的特殊性

看過《高性能JavaScript》這本書的小夥伴都知道,JavaScipt的語言特性、數據結構和算法、瀏覽器機理、網絡傳輸等均可能致使性能問題。一樣是web實現,跟傳統的技術(如原生js、jQuery)相比, react的性能優化有什麼不一樣呢?git

使用jQuery時,要考慮怎麼使用選擇器來提升元素查找效率、不要在循環體內進行DOM操做、使用事件委託呀等等。到了React這裏,這些東西好像都用不上了。是的,由於React有一個很大的不一樣點,它實現了虛擬DOM,而且接管了DOM的操做。你不能直接去操做DOM來改變UI,你只能經過改變數據源(props和state)來驅動UI的變化。github

提及React的性能分析,還得從它的生命週期和渲染機制提及:web

React組件生命週期

圖片描述

當 props 和 state 發生變化時,React會根據shouldComponentUpdate方法來決定是否從新渲染整個組件。算法

React組件樹渲染機制

圖片描述

父親組件的props 和 state發生變化時,它和它的子組件、孫子組件等全部後代組件都會從新渲染。chrome


綜上所述,能夠得出React的性能優化就是圍繞shouldComponentUpdate方法(SCU)來進行的,無外乎兩點:瀏覽器

  1. 縮短SCU方法的執行時間(或者不執行)。

  2. 不必的渲染,SCU應該返回false。

React 性能分析工具

Web通用工具:Chrome DevTools

最經常使用到的是Chrome DevTools的Timeline和Profiles。

  • Timeline工具欄提供了對於在裝載你的Web應用的過程當中,時間花費狀況的概覽,這些應用包括處理DOM事件, 頁面佈局渲染或者向屏幕繪製元素。

  • 經過Timeline發現是腳本問題時,使用Profiles做進一步分析。Profiles能夠提供更加詳細的腳本信息。

React特點工具:Perf

Perf 是react官方提供的性能分析工具。Perf最核心的方法莫過於Perf.printWasted(measurements)了,該方法會列出那些不必的組件渲染。很大程度上,React的性能優化就是幹掉這些無謂的渲染。

有童鞋開發了Chrome擴展程序「React Perf」(戳這裏)。相比本身在代碼中插入Perf方法進行分析,這個小工具更加靈活方便,牆裂推薦!

案例分析:TodoList

TodoList的功能很簡單,就是對待辦事項進行增長和刪除操做:
圖片描述

import React, {PropTypes, Component} from 'react';

class TodoItem extends Component {

    static propTypes = {
        deleteItem: PropTypes.func.isRequired,
        item: PropTypes.shape({
            text: PropTypes.string.isRequired,
            id: PropTypes.number.isRequired,
        }).isRequired,
    };

    deleteItem = ()=>{
        let id = this.props.item.id;
        this.props.deleteItem(id);
    };

    render() {
        return (
            <div>
                <button style={{width: 30}} onClick={this.deleteItem}>X</button>
                &nbsp;
                <span>{this.props.item.text}</span>
            </div>
        );
    }

}

class Todos extends Component {

    // 構造
    constructor(props) {
        super(props);
        // 初始狀態
        this.state = {
            items: this.props.initialItems,
            text: '',
        };
    }

    static propTypes = {
        initialItems: PropTypes.arrayOf(PropTypes.shape({
            text: PropTypes.string.isRequired,
            id: PropTypes.number.isRequired,
        }).isRequired).isRequired,
    };

    addTask = (e)=> {
        e.preventDefault();
        this.setState({
            items: [{id: ID++, text: this.state.text}].concat(this.state.items),
            text: '',
        });
    };

    deleteItem = (itemId)=> {
        this.setState({
            items: this.state.items.filter((item) => item.id !== itemId),
        });
    };

    render() {
        return (
            <div>
                <h1>待辦事項</h1>
                <form onSubmit={this.addTask}>
                    <input value={this.state.text} onChange={(v)=>{this.setState({text:v.target.value});}}/>
                    <button>添加</button>
                </form>
                {this.state.items.map((item) => {
                    return (
                        <TodoItem key={item.id}
                                  item={item}
                                  deleteItem={this.deleteItem}/>
                    );
                })}
            </div>
        );
    }
}

let ID = 0;
const items = [];
for (let i = 0; i < 1000; i++) {
    items.push({id: ID++, text: '事項' + i});
}

class TodoList extends Component {
    render() {
        return (
            <Todos initialItems={items}/>
        );
    }
}

export default TodoList;

在待辦事項輸入框裏輸入一個字母,接下來咱們以這個行爲爲例來進行性能分析和優化。

第一次優化

使用Chrome開發者工具的Timeline記錄下這個過程:
clipboard.png

重點關注出現的紅色塊,表明這個行爲存在性能問題。從上圖咱們能夠看出,耗時的Event(keypress)長條花了98.8ms,其中98.5ms用於腳本處理,可見腳本問題是罪魁禍首。

接着,咱們使用Profiles來進一步分析腳本問題:
clipboard.png

對Total Time進行降序排列,發現耗時最長的是dispatchEvent,來自react源碼。這時,咱們就能夠肯定是react這一層出現了性能問題。

嗯,輪到Perf出場了:
clipboard.png

上圖表示,有1000次沒必要要的渲染髮生在TodoItem組件上.

打開react面板,咱們來看看組件的層次和相應的state、props值:
clipboard.png

TodoItem是Todos的子組件,當咱們在輸入框輸入字母「s」時,Todos的state值發生改變時,文章開頭所說的react的渲染機制致使Todos下的1000個TodoItem組件都會從新渲染一次。可是,TodoItem的展示其實沒有任何變化。
從代碼中,咱們能夠看出,TodoItem組件展示只跟props(deleteItem、item)相關。props沒有變化,TodoItem就不必渲染。

因此,咱們應該優化下TodoItem的SCU方法:

class TodoItem extends Component {
    
    ...
    
    //在props沒有變化的時候返回false,不從新渲染
    shouldComponentUpdate(nextState,nextProps) {
        if(this.props.item == nextProps.item && this.props.deleteItem == nextProps.deleteItem){
            return false;
        }
        return true;
    }

    render() {
       ... 
    }

}

(PS: TodoItem中的SCU方法,使用的是淺比較,也可使用PureComponent代替。實際項目中,每每須要使用複雜的深比較,能夠考慮使用Immutable.js)

驗證下優化效果,使用Perf測試,發現1000個多餘的渲染被幹掉了!
再次使用Timeline分析,Event(keypress)耗時從98.5ms降到了26.49ms,性能提高了2.7倍:
clipboard.png

療效還不錯!

第二次優化

經過SCU返回false,咱們避免了無謂的渲染。可是,咱們仍是調用了1000次TodoItem的SCU方法,這也是一筆不小的性能開支。

是否能夠不用調用呢?經過合理地規劃組件粒度,能夠作到:

//將增長待辦事項抽象成一個組件
class AddItem extends Component{
     constructor(props) {
       super(props);
       this.state = {
           text:""
       };
     }

    static PropTypes = {
      addTask:PropTypes.func.isRequired
    };

    addTask = (e)=>{
        e.preventDefault();
        this.props.addTask(this.state.text);
    };

    render(){
        return (
            <form onSubmit={this.addTask}>
                <input value={this.state.text} onChange={(v)=>{this.setState({text:v.target.value});}}/>
                <button>添加</button>
            </form>
        );

    }
}

class Todos extends Component{
    constructor(props) {
        super(props);
        this.state = {
            items: this.props.initialItems,
        };
    }

    static propTypes = {
        initialItems: PropTypes.arrayOf(PropTypes.shape({
            text: PropTypes.string.isRequired,
            id: PropTypes.number.isRequired,
        }).isRequired).isRequired,
    };

    addTask = (text)=>{
        this.setState({
            items: [{id: ID++, text:text}].concat(this.state.items),
            text: '',
        });
    };

    deleteItem = (itemId)=>{
        this.setState({
            items: this.state.items.filter((item) => item.id !== itemId),
        });
    };

    render() {
        return (
            <div>
                <h1>待辦事項V3</h1>
                <AddItem addTask={this.addTask}/>
                {this.state.items.map((item) => {
                    return (
                        <TodoItem key={item.id}
                                  item={item}
                                  deleteItem={this.deleteItem}/>
                    );
                })}
            </div>
        );
    }
}

把增長待辦事項抽象成一個AddItem組件。這樣一來,組件樹從原來的
clipboard.png

變成

clipboard.png

輸入信息時觸發變化的text這個state值,被下放到AddItem組件來管理,所以不會致使兄弟組件(TodoItem)的從新渲染。

再次運行Timeline測試,這時Event(keypress)耗時從26.49ms降到了7.98ms,性能提高了2.3倍:
clipboard.png

至此,性能優化完畢~

相關文章
相關標籤/搜索