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優化