筆者是一個 react
重度愛好者,在工做之餘,也看了很多的 react
文章, 寫了不少 react
項目 ,接下來筆者討論一下 React 性能優化的主要方向和一些工做中的小技巧。送人玫瑰,手留餘香,閱讀的朋友能夠給筆者點贊,關注一波 。 陸續更新前端文章。css
本文篇幅較長,將從 編譯階段 -> 路由階段 -> 渲染階段 -> 細節優化 -> 狀態管理 -> 海量數據源,長列表渲染
方向分別加以探討。前端
當咱們用create-react-app
或者webpack
構建react
工程的時候,有沒有想過一個問題,咱們的配置可否讓咱們的項目更快的構建速度,更小的項目體積,更簡潔清晰的項目結構。 隨着咱們的項目越作越大,項目依賴愈來愈多,項目結構愈來愈來複雜,項目體積就會愈來愈大,構建時間愈來愈長,長此以往就會成了一個又大又重的項目,因此說咱們要學會適當的爲項目‘減負’,讓項目不能輸在起跑線上。vue
拿咱們以前接觸過的一個react
老項目爲例。咱們沒有用dva
,umi
快速搭建react,而是用react
老版本腳手架構建的,這對這種老的react
項目,上述的問題都會存在,下面讓咱們一塊兒來看看。node
咱們首先看一下項目結構。 react
再看看構建時間。webpack
爲了方便你們看構建時間,我簡單寫了一個webpack,plugin
ConsolePlugin
,記錄了webpack
在一次compilation
所用的時間。web
const chalk = require('chalk') /* console 顏色 */
var slog = require('single-line-log'); /* 單行打印 console */
class ConsolePlugin {
constructor(options){
this.options = options
}
apply(compiler){
/** * Monitor file change 記錄當前改動文件 */
compiler.hooks.watchRun.tap('ConsolePlugin', (watching) => {
const changeFiles = watching.watchFileSystem.watcher.mtimes
for(let file in changeFiles){
console.log(chalk.green('當前改動文件:'+ file))
}
})
/** * before a new compilation is created. 開始 compilation 編譯 。 */
compiler.hooks.compile.tap('ConsolePlugin',()=>{
this.beginCompile()
})
/** * Executed when the compilation has completed. 一次 compilation 完成。 */
compiler.hooks.done.tap('ConsolePlugin',()=>{
this.timer && clearInterval( this.timer )
const endTime = new Date().getTime()
const time = (endTime - this.starTime) / 1000
console.log( chalk.yellow(' 編譯完成') )
console.log( chalk.yellow('編譯用時:' + time + '秒' ) )
})
}
beginCompile(){
const lineSlog = slog.stdout
let text = '開始編譯:'
/* 記錄開始時間 */
this.starTime = new Date().getTime()
this.timer = setInterval(()=>{
text += '█'
lineSlog( chalk.green(text))
},50)
}
}
複製代碼
構建時間以下:ajax
打包後的體積:算法
針對上面這個react
老項目,咱們開始針對性的優化。因爲本文主要講的是react
,因此咱們不把太多篇幅給webpack優化
上。redux
{
test: /\.jsx?$/,
exclude: /node_modules/,
include: path.resolve(__dirname, '../src'),
use:['happypack/loader?id=babel']
// loader: 'babel-loader'
}
複製代碼
除了上述改動以外,在plugin中
/* 多線程編譯 */
new HappyPack({
id:'babel',
loaders:['babel-loader?cacheDirectory=true']
})
複製代碼
loaders:['babel-loader?cacheDirectory=true']
複製代碼
優化後項目結構
優化構建時間以下:
一次 compilation
時間 從23秒優化到了4.89秒
優化打包後的體積:
因而可知,若是咱們的react
是本身徒手搭建的,一些優化技巧顯得格外重要。
咱們在作react
項目的時候,會用到antd
之類的ui庫,值得思考的一件事是,若是咱們只是用到了antd
中的個別組件,好比<Button />
,就要把整個樣式庫引進來,打包就會發現,體積由於引入了整個樣式大了不少。咱們能夠經過.babelrc
實現按需引入。
瘦身前
.babelrc
增長對 antd
樣式按需引入。
["import", {
"libraryName":
"antd",
"libraryDirectory": "es",
"style": true
}]
複製代碼
瘦身後
若是想要優化react
項目,從構建開始是必不可少的。咱們要重視從構建到打包上線的每個環節。
react
路由懶加載,是筆者看完dva
源碼中的 dynamic
異步加載組件總結出來的,針對大型項目有不少頁面,在配置路由的時候,若是沒有對路由進行處理,一次性會加載大量路由,這對頁面初始化很不友好,會延長頁面初始化時間,因此咱們想這用asyncRouter
來按需加載頁面路由。
若是咱們沒有用umi
等框架,須要手動配置路由的時候,也許路由會這樣配置。
<Switch>
<Route path={'/index'} component={Index} ></Route>
<Route path={'/list'} component={List} ></Route>
<Route path={'/detail'} component={ Detail } ></Route>
<Redirect from='/*' to='/index' />
</Switch>
複製代碼
或者用list保存路由信息,方便在進行路由攔截,或者配置路由菜單等。
const router = [
{
'path': '/index',
'component': Index
},
{
'path': '/list'', 'component': List }, { 'path': '/detail', 'component': Detail }, ] 複製代碼
咱們今天講的這種react
路由懶加載是基於import
函數路由懶加載, 衆所周知 ,import
執行會返回一個Promise
做爲異步加載的手段。咱們能夠利用這點來實現react
異步加載路由
好的一言不合上代碼。。。
代碼
const routerObserveQueue = [] /* 存放路由衛視鉤子 */
/* 懶加載路由衛士鉤子 */
export const RouterHooks = {
/* 路由組件加載以前 */
beforeRouterComponentLoad: function(callback) {
routerObserveQueue.push({
type: 'before',
callback
})
},
/* 路由組件加載以後 */
afterRouterComponentDidLoaded(callback) {
routerObserveQueue.push({
type: 'after',
callback
})
}
}
/* 路由懶加載HOC */
export default function AsyncRouter(loadRouter) {
return class Content extends React.Component {
constructor(props) {
super(props)
/* 觸發每一個路由加載以前鉤子函數 */
this.dispatchRouterQueue('before')
}
state = {Component: null}
dispatchRouterQueue(type) {
const {history} = this.props
routerObserveQueue.forEach(item => {
if (item.type === type) item.callback(history)
})
}
componentDidMount() {
if (this.state.Component) return
loadRouter()
.then(module => module.default)
.then(Component => this.setState({Component},
() => {
/* 觸發每一個路由加載以後鉤子函數 */
this.dispatchRouterQueue('after')
}))
}
render() {
const {Component} = this.state
return Component ? <Component { ...this.props } /> : null
}
}
}
複製代碼
asyncRouter
實際就是一個高級組件,將()=>import()
做爲加載函數傳進來,而後當外部Route
加載當前組件的時候,在componentDidMount
生命週期函數,加載真實的組件,並渲染組件,咱們還能夠寫針對路由懶加載狀態定製屬於本身的路由監聽器beforeRouterComponentLoad
和afterRouterComponentDidLoaded
,相似vue
中 watch $route
功能。接下來咱們看看如何使用。
使用
import AsyncRouter ,{ RouterHooks } from './asyncRouter.js'
const { beforeRouterComponentLoad} = RouterHooks
const Index = AsyncRouter(()=>import('../src/page/home/index'))
const List = AsyncRouter(()=>import('../src/page/list'))
const Detail = AsyncRouter(()=>import('../src/page/detail'))
const index = () => {
useEffect(()=>{
/* 增長監聽函數 */
beforeRouterComponentLoad((history)=>{
console.log('當前激活的路由是',history.location.pathname)
})
},[])
return <div > <div > <Router > <Meuns/> <Switch> <Route path={'/index'} component={Index} ></Route> <Route path={'/list'} component={List} ></Route> <Route path={'/detail'} component={ Detail } ></Route> <Redirect from='/*' to='/index' /> </Switch> </Router> </div> </div>
}
複製代碼
效果
這樣一來,咱們既作到了路由的懶加載,又彌補了react-router
沒有監聽當前路由變化的監聽函數的缺陷。
可控性組件顆粒化,獨立請求服務渲染單元是筆者在實際工做總結出來的經驗。目的就是避免因自身的渲染更新或是反作用帶來的全局從新渲染。
可控性組件和非可控性的區別就是dom
元素值是否與受到react
數據狀態state
控制。一旦由react的state
控制數據狀態,好比input
輸入框的值,就會形成這樣一個場景,爲了使input
值實時變化,會不斷setState
,就會不斷觸發render
函數,若是父組件內容簡單還好,若是父組件比較複雜,會形成牽一髮動全身,若是其餘的子組件中componentWillReceiveProps
這種帶有反作用的鉤子,那麼引起的蝴蝶效應不敢想象。好比以下demo
。
class index extends React.Component<any,any>{
constructor(props){
super(props)
this.state={
inputValue:''
}
}
handerChange=(e)=> this.setState({ inputValue:e.target.value })
render(){
const { inputValue } = this.state
return <div> { /* 咱們增長三個子組件 */ } <ComA /> <ComB /> <ComC /> <div className="box" > <Input value={inputValue} onChange={ (e)=> this.handerChange(e) } /> </div> {/* 咱們首先來一個列表循環 */} { new Array(10).fill(0).map((item,index)=>{ console.log('列表循環了' ) return <div key={index} >{item}</div> }) } { /* 這裏多是更復雜的結構 */ /* ------------------ */ } </div>
}
}
複製代碼
組件A
function index(){
console.log('組件A渲染')
return <div>我是組件A</div>
}
複製代碼
組件B,有一個componentWillReceiveProps鉤子
class Index extends React.Component{
constructor(props){
super(props)
}
componentWillReceiveProps(){
console.log('componentWillReceiveProps執行')
/* 可能作一些騷操做 wu lian */
}
render(){
console.log('組件B渲染')
return <div> 我是組件B </div>
}
}
複製代碼
組件C有一個列表循環
class Index extends React.Component{
constructor(props){
super(props)
}
render(){
console.log('組件c渲染')
return <div> 我是組件c { new Array(10).fill(0).map((item,index)=>{ console.log('組件C列表循環了' ) return <div key={index} >{item}</div> }) } </div>
}
}
複製代碼
效果
當咱們在input輸入內容的時候。就會形成如上的現象,全部的不應從新更新的地方,所有從新執行了一遍,這無疑是巨大的性能損耗。這個一個setState
觸發帶來的一股巨大的由此組件到子組件可能更深的更新流,帶來的反作用是不可估量的。因此咱們能夠思考一下,是否將這種受控性組件顆粒化,讓本身更新 -> 渲染過程由自身調度。
說幹就幹,咱們對上面的input表單單獨顆粒化處理。
const ComponentInput = memo(function({ notifyFatherChange }:any){
const [ inputValue , setInputValue ] = useState('')
const handerChange = useMemo(() => (e) => {
setInputValue(e.target.value)
notifyFatherChange && notifyFatherChange(e.target.value)
},[])
return <Input value={inputValue} onChange={ handerChange } />
})
複製代碼
此時的組件更新由組件單元自行控制,不須要父組件的更新,因此不須要父組件設置獨立state
保留狀態。只須要綁定到this
上便可。不是全部狀態都應該放在組件的 state 中. 例如緩存數據。若是須要組件響應它的變更, 或者須要渲染到視圖中的數據才應該放到 state 中。這樣能夠避免沒必要要的數據變更致使組件從新渲染.
class index extends React.Component<any,any>{
formData :any = {}
render(){
return <div> { /* 咱們增長三個子組件 */ } <ComA /> <ComB /> <ComC /> <div className="box" > <ComponentInput notifyFatherChange={ (value)=>{ this.formData.inputValue = value } } /> <Button onClick={()=> console.log(this.formData)} >打印數據</Button> </div> {/* 咱們首先來一個列表循環 */} { new Array(10).fill(0).map((item,index)=>{ console.log('列表循環了' ) return <div key={index} >{item}</div> }) } { /* 這裏多是更復雜的結構 */ /* ------------------ */ } </div>
}
}
複製代碼
效果
這樣除了當前組件外,其餘地方沒有收到任何渲染波動,達到了咱們想要的目的。
創建獨立的請求渲染單元,直接理解就是,若是咱們把頁面,分爲請求數據展現部分(經過調用後端接口,獲取數據),和基礎部分(不須要請求數據,已經直接寫好的),對於一些邏輯交互不是很複雜的數據展現部分,我推薦用一種獨立組件,獨立請求數據,獨立控制渲染的模式。至於爲何咱們能夠慢慢分析。
首先咱們看一下傳統的頁面模式。
頁面有三個展現區域分別,作了三次請求,觸發了三次setState
,渲染三次頁面,即便用Promise.all
等方法,可是也不保證接下來交互中,會有部分展現區從新拉取數據的可能。一旦有一個區域從新拉取數據,另外兩個區域也會說、受到牽連,這種效應是不可避免的,即使react有很好的ddiff
算法去調協相同的節點,可是好比長列表等狀況,循環在所不免。
class Index extends React.Component{
state :any={
dataA:null,
dataB:null,
dataC:null
}
async componentDidMount(){
/* 獲取A區域數據 */
const dataA = await getDataA()
this.setState({ dataA })
/* 獲取B區域數據 */
const dataB = await getDataB()
this.setState({ dataB })
/* 獲取C區域數據 */
const dataC = await getDataC()
this.setState({ dataC })
}
render(){
const { dataA , dataB , dataC } = this.state
console.log(dataA,dataB,dataC)
return <div> <div> { /* 用 dataA 數據作展現渲染 */ } </div> <div> { /* 用 dataB 數據作展現渲染 */ } </div> <div> { /* 用 dataC 數據作展現渲染 */ } </div> </div>
}
}
複製代碼
接下來咱們,把每一部分抽取出來,造成獨立的渲染單元,每一個組件都獨立數據請求到獨立渲染。
function ComponentA(){
const [ dataA, setDataA ] = useState(null)
useEffect(()=>{
getDataA().then(res=> setDataA(res.data) )
},[])
return <div> { /* 用 dataA 數據作展現渲染 */ } </div>
}
function ComponentB(){
const [ dataB, setDataB ] = useState(null)
useEffect(()=>{
getDataB().then(res=> setDataB(res.data) )
},[])
return <div> { /* 用 dataB 數據作展現渲染 */ } </div>
}
function ComponentC(){
const [ dataC, setDataC ] = useState(null)
useEffect(()=>{
getDataC().then(res=> setDataC(res.data) )
},[])
return <div> { /* 用 dataC 數據作展現渲染 */ } </div>
}
function Index (){
return <div> <ComponentA /> <ComponentB /> <ComponentC /> </div>
}
複製代碼
這樣一來,彼此的數據更新都不會相互影響。
拆分須要單獨調用後端接口的細小組件,創建獨立的數據請求和渲染,這種依賴數據更新 -> 視圖渲染的組件,能從整個體系中抽離出來 ,好處我總結有如下幾個方面。
1 能夠避免父組件的冗餘渲染 ,react
的數據驅動,依賴於 state
和 props
的改變,改變state
必然會對組件 render
函數調用,若是父組件中的子組件過於複雜,一個自組件的 state
改變,就會牽一髮動全身,必然影響性能,因此若是把不少依賴請求的組件抽離出來,能夠直接減小渲染次數。
2 能夠優化組件自身性能,不管從class
聲明的有狀態組件仍是fun
聲明的無狀態,都有一套自身優化機制,不管是用shouldupdate
仍是用 hooks
中 useMemo
useCallback
,均可以根據自身狀況,定製符合場景的渲條 件,使得依賴數據請求組件造成本身一個小的,適合自身的渲染環境。
3 可以和redux
,以及redux
衍生出來 redux-action
, dva
,更加契合的工做,用 connect
包裹的組件,就能經過制定好的契約,根據所需求的數據更新,而更新自身,而把這種模式用在這種小的,須要數據驅動的組件上,就會起到物盡其用的效果。
在這裏咱們拿immetable.js
爲例,講最傳統的限制更新方法,第六部分將要將一些避免從新渲染的細節。
React.PureComponent
與 React.Component
用法差很少 ,但 React.PureComponent
經過props和state的淺對比來實現 shouldComponentUpate()
。若是對象包含複雜的數據結構(好比對象和數組),他會淺比較,若是深層次的改變,是沒法做出判斷的,React.PureComponent
認爲沒有變化,而沒有渲染試圖。
如這個例子
class Text extends React.PureComponent<any,any>{
render(){
console.log(this.props)
return <div>hello,wrold</div>
}
}
class Index extends React.Component<any,any>{
state={
data:{ a : 1 , b : 2 }
}
handerClick=()=>{
const { data } = this.state
data.a++
this.setState({ data })
}
render(){
const { data } = this.state
return <div> <button onClick={ this.handerClick } >點擊</button> <Text data={data} /> </div>
}
}
複製代碼
效果
咱們點擊按鈕,發現 <Text />
根本沒有從新更新。這裏雖然改了data
可是隻是改變了data
下的屬性,因此 PureComponent
進行淺比較不會update
。
想要解決這個問題實際也很容易。
<Text data={{ ...data }} />
複製代碼
不管組件是不是 PureComponent
,若是定義了 shouldComponentUpdate()
,那麼會調用它並以它的執行結果來判斷是否 update
。在組件未定義 shouldComponentUpdate()
的狀況下,會判斷該組件是不是 PureComponent
,若是是的話,會對新舊 props、state
進行 shallowEqual
比較,一旦新舊不一致,會觸發渲染更新。
react.memo
和 PureComponent
功能相似 ,react.memo
做爲第一個高階組件,第二個參數 能夠對props
進行比較 ,和shouldComponentUpdate
不一樣的, 當第二個參數返回 true
的時候,證實props
沒有改變,不渲染組件,反之渲染組件。
使用 shouldComponentUpdate()
以讓React
知道當state或props
的改變是否影響組件的從新render
,默認返回ture
,返回false
時不會從新渲染更新,並且該方法並不會在初始化渲染或當使用 forceUpdate()
時被調用,一般一個shouldComponentUpdate
應用是這麼寫的。
控制狀態
shouldComponentUpdate(nextProps, nextState) {
/* 當 state 中 data1 發生改變的時候,從新更新組件 */
return nextState.data1 !== this.state.data1
}
複製代碼
這個的意思就是 僅當state
中 data1
發生改變的時候,從新更新組件。 控制prop屬性
shouldComponentUpdate(nextProps, nextState) {
/* 當 props 中 data2發生改變的時候,從新更新組件 */
return nextProps.data2 !== this.props.data2
}
複製代碼
這個的意思就是 僅當props
中 data2
發生改變的時候,從新更新組件。
immetable.js
是Facebook 開發的一個js
庫,能夠提升對象的比較性能,像以前所說的pureComponent
只能對對象進行淺比較,,對於對象的數據類型,卻一籌莫展,因此咱們能夠用 immetable.js
配合 shouldComponentUpdate
或者 react.memo
來使用。immutable
中
咱們用react-redux
來簡單舉一個例子,以下所示 數據都已經被 immetable.js
處理。
import { is } from 'immutable'
const GoodItems = connect(state =>
({ GoodItems: filter(state.getIn(['Items', 'payload', 'list']), state.getIn(['customItems', 'payload', 'list'])) || Immutable.List(), })
/* 此處省略不少代碼~~~~~~ */
)(memo(({ Items, dispatch, setSeivceId }) => {
/* */
}, (pre, next) => is(pre.Items, next.Items)))
複製代碼
經過 is
方法來判斷,先後Items
(對象數據類型)是否發生變化。
有的時候,咱們在敲代碼的時候,稍微注意如下,就能避免性能的開銷。也許只是稍加改動,就能其餘優化性能的效果。
衆所周知,react
更新來大部分狀況自於props
的改變(被動渲染),和state
改變(主動渲染)。當咱們給未加任何更新限定條件子組件綁定事件的時候,或者是PureComponent
純組件, 若是咱們箭頭函數使用的話。
<ChildComponent handerClick={()=>{ console.log(666) }} />
複製代碼
每次渲染時都會建立一個新的事件處理器,這會致使 ChildComponent
每次都會被渲染。
即使咱們用箭頭函數綁定給dom
元素。
<div onClick={ ()=>{ console.log(777) } } >hello,world</div>
複製代碼
每次react
合成事件事件的時候,也都會從新聲明一個新事件。
解決這個問題事件很簡單,分爲無狀態組件和有狀態組件。
有狀態組件
class index extends React.Component{
handerClick=()=>{
console.log(666)
}
handerClick1=()=>{
console.log(777)
}
render(){
return <div> <ChildComponent handerClick={ this.handerClick } /> <div onClick={ this.handerClick1 } >hello,world</div> </div>
}
}
複製代碼
無狀態組件
function index(){
const handerClick1 = useMemo(()=>()=>{
console.log(777)
},[]) /* [] 存在當前 handerClick1 的依賴項*/
const handerClick = useCallback(()=>{ console.log(666) },[]) /* [] 存在當前 handerClick 的依賴項*/
return <div> <ChildComponent handerClick={ handerClick } /> <div onClick={ handerClick1 } >hello,world</div> </div>
}
複製代碼
對於dom
,若是咱們須要傳遞參數。咱們能夠這麼寫。
function index(){
const handerClick1 = useMemo(()=>(event)=>{
const mes = event.currentTarget.dataset.mes
console.log(mes) /* hello,world */
},[])
return <div> <div data-mes={ 'hello,world' } onClick={ handerClick1 } >hello,world</div> </div>
}
複製代碼
不管是react
和 vue
,正確使用key
,目的就是在一次循環中,找到與新節點對應的老節點,複用節點,節省開銷。想深刻理解的同窗能夠看一下筆者的另一篇文章 全面解析 vue3.0 diff算法 裏面有對key
詳細說明。咱們今天來看如下key
正確用法,和錯誤用法。
錯誤用法一:用index作key
function index(){
const list = [ { id:1 , name:'哈哈' } , { id:2, name:'嘿嘿' } ,{ id:3 , name:'嘻嘻' } ]
return <div> <ul> { list.map((item,index)=><li key={index} >{ item.name }</li>) } </ul> </div>
}
複製代碼
這種加key
的性能,實際和不加key
效果差很少,每次仍是從頭至尾diff。
錯誤用法二:用index拼接其餘的字段
function index(){
const list = [ { id:1 , name:'哈哈' } , { id:2, name:'嘿嘿' } ,{ id:3 , name:'嘻嘻' } ]
return <div> <ul> { list.map((item,index)=><li key={index + item.name } >{ item.name }</li>) } </ul> </div>
}
複製代碼
若是有元素移動或者刪除,那麼就失去了一一對應關係,剩下的節點都不能有效複用。
正確用法:用惟一id做爲key
function index(){
const list = [ { id:1 , name:'哈哈' } , { id:2, name:'嘿嘿' } ,{ id:3 , name:'嘻嘻' } ]
return <div> <ul> { list.map((item,index)=><li key={ item.id } >{ item.name }</li>) } </ul> </div>
}
複製代碼
用惟一的健id
做爲key
,可以作到有效複用元素節點。
hooks-useMemo
避免重複聲明。對於無狀態組件,數據更新就等於函數上下文的重複執行。那麼函數裏面的變量,方法就會從新聲明。好比以下狀況。
function Index(){
const [ number , setNumber ] = useState(0)
const handerClick1 = ()=>{
/* 一些操做 */
}
const handerClick2 = ()=>{
/* 一些操做 */
}
const handerClick3 = ()=>{
/* 一些操做 */
}
return <div> <a onClick={ handerClick1 } >點我有驚喜1</a> <a onClick={ handerClick2 } >點我有驚喜2</a> <a onClick={ handerClick3 } >點我有驚喜3</a> <button onClick={ ()=> setNumber(number+1) } > 點擊 { number } </button> </div>
}
複製代碼
每次點擊button
的時候,都會執行Index
函數。handerClick1
, handerClick2
,handerClick3
都會從新聲明。爲了不這個狀況的發生,咱們能夠用 useMemo
作緩存,咱們能夠改爲以下。
function Index(){
const [ number , setNumber ] = useState(0)
const [ handerClick1 , handerClick2 ,handerClick3] = useMemo(()=>{
const fn1 = ()=>{
/* 一些操做 */
}
const fn2 = ()=>{
/* 一些操做 */
}
const fn3= ()=>{
/* 一些操做 */
}
return [fn1 , fn2 ,fn3]
},[]) /* 只有當數據裏面的依賴項,發生改變的時候,纔會從新聲明函數。 */
return <div> <a onClick={ handerClick1 } >點我有驚喜1</a> <a onClick={ handerClick2 } >點我有驚喜2</a> <a onClick={ handerClick3 } >點我有驚喜3</a> <button onClick={ ()=> setNumber(number+1) } > 點擊 { number } </button> </div>
}
複製代碼
以下改變以後,handerClick1
, handerClick2
,handerClick3
會被緩存下來。
Suspense
和 lazy
能夠實現 dynamic import
懶加載效果,原理和上述的路由懶加載差很少。在 React
中的使用方法是在 Suspense
組件中使用 <LazyComponent>
組件。
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function demo () {
return (
<div> <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> </div>
)
}
複製代碼
LazyComponent
是經過懶加載加載進來的,因此渲染頁面的時候可能會有延遲,但使用了 Suspense
以後,在加載狀態下,能夠用<div>Loading...</div>
做爲loading
效果。
Suspense
能夠包裹多個懶加載組件。
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
<LazyComponent1 />
</Suspense>
複製代碼
避免重複渲染,是react
性能優化的重要方向。若是想全力以赴處理好react
項目每個細節,那麼就要從每一行代碼開始,從每一組件開始。正所謂不積硅步無以致千里。
此次講的批量更新的概念,實際主要是針對無狀態組件和hooks
中useState
,和 class
有狀態組件中的this.setState
,兩種方法已經作了批量更新的處理。好比以下例子
一次更新中
class index extends React.Component{
constructor(prop){
super(prop)
this.state = {
a:1,
b:2,
c:3,
}
}
handerClick=()=>{
const { a,b,c } :any = this.state
this.setState({ a:a+1 })
this.setState({ b:b+1 })
this.setState({ c:c+1 })
}
render= () => <div onClick={this.handerClick} />
}
複製代碼
點擊事件發生以後,會觸發三次 setState
,可是不會渲染三次,由於有一個批量更新batchUpdate
批量更新的概念。三次setState
最後被合成相似以下樣子
this.setState({
a:a+1 ,
b:b+1 ,
c:c+1
})
複製代碼
無狀態組件中
const [ a , setA ] = useState(1)
const [ b , setB ] = useState({})
const [ c , setC ] = useState(1)
const handerClick = () => {
setB( { ...b } )
setC( c+1 )
setA( a+1 )
}
複製代碼
當咱們針對上述兩種狀況加以以下處理以後。
handerClick=()=>{
setTimeout(() => {
this.setState({ a:a+1 })
this.setState({ b:b+1 })
this.setState({ c:c+1 })
}, 0)
}
複製代碼
const handerClick = () => {
Promise.resolve().then(()=>{
setB( { ...b } )
setC( c+1 )
setA( a+1 )
})
}
複製代碼
咱們會發現,上述兩種狀況 ,組件都更新渲染了三次 ,此時的批量更新失效了。這種狀況在react-hooks
中也廣泛存在,這種狀況甚至在hooks
中更加明顯,由於咱們都知道hooks
中每一個useState
保存了一個狀態,並非讓class
聲明組件中,能夠經過this.state
統一協調狀態,再一次異步函數中,好比說一次ajax
請求後,想經過多個useState
改變狀態,會形成屢次渲染頁面,爲了解決這個問題,咱們能夠手動批量更新。
react-dom
中提供了unstable_batchedUpdates
方法進行手動批量更新。這個api
更契合react-hooks
,咱們能夠這樣作。
const handerClick = () => {
Promise.resolve().then(()=>{
unstable_batchedUpdates(()=>{
setB( { ...b } )
setC( c+1 )
setA( a+1 )
})
})
}
複製代碼
這樣三次更新,就會合併成一次。一樣達到了批量更新的效果。
合併state
這種,是一種咱們在react
項目開發中要養成的習慣。我看過有些同窗的代碼中可能會這麼寫(以下demo
是模擬的狀況,實際要比這複雜的多)。
class Index extends React.Component<any , any>{
state = {
loading:false /* 用來模擬loading效果 */,
list:[],
}
componentDidMount(){
/* 模擬一個異步請求數據場景 */
this.setState({ loading : true }) /* 開啓loading效果 */
Promise.resolve().then(()=>{
const list = [ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ]
this.setState({ loading : false },()=>{
this.setState({
list:list.map(item=>({
...item,
name:item.name.toLocaleUpperCase()
}))
})
})
})
}
render(){
const { list } = this.state
return <div>{ list.map(item=><div key={item.id} >{ item.name }</div>) }</div>
}
}
複製代碼
分別用兩次this.state
第一次解除loading
狀態,第二次格式化數據列表。這另兩次更新徹底沒有必要,能夠用一次setState
更新完美解決。不這樣作的緣由是,對於像demo
這樣的簡單結構還好,對於複雜的結構,一次更新可能都是寶貴的,因此咱們應該學會去合併state。將上述demo這樣修改。
this.setState({
loading : false,
list:list.map(item=>({
...item,
name:item.name.toLocaleUpperCase()
}))
})
複製代碼
對於無狀態組件,咱們能夠經過一個useState
保存多個狀態,沒有必要每個狀態都用一個useState
。
對於這樣的狀況。
const [ a ,setA ] = useState(1)
const [ b ,setB ] = useState(2)
複製代碼
咱們徹底能夠一個state
搞定。
const [ numberState , setNumberState ] = useState({ a:1 , b :2})
複製代碼
可是要注意,若是咱們的state已經成爲 useEffect
, useCallback
, useMemo
依賴項,請慎用如上方法。
react
正常的更新流,就像利劍一下,從父組件項子組件穿透,爲了不這些重複的更新渲染,shouldComponentUpdate
, React.memo
等api
也應運而生。可是有的狀況下,多餘的更新在所不免,好比以下這種狀況。這種更新會由父組件 -> 子組件 傳遞下去。
function ChildrenComponent(){
console.log(2222)
return <div>hello,world</div>
}
function Index (){
const [ list ] = useState([ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ])
const [ number , setNumber ] = useState(0)
return <div> <span>{ number }</span> <button onClick={ ()=> setNumber(number + 1) } >點擊</button> <ul> { list.map(item=>{ console.log(1111) return <li key={ item.id } >{ item.name }</li> }) } </ul> <ChildrenComponent /> </div>
}
複製代碼
效果
針對這一現象,咱們能夠經過使用useMemo
進行隔離,造成獨立的渲染單元,每次更新上一個狀態會被緩存,循環不會再執行,子組件也不會再次被渲染,咱們能夠這麼作。
function Index (){
const [ list ] = useState([ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ])
const [ number , setNumber ] = useState(0)
return <div> <span>{ number }</span> <button onClick={ ()=> setNumber(number + 1) } >點擊</button> <ul> { useMemo(()=>(list.map(item=>{ console.log(1111) return <li key={ item.id } >{ item.name }</li> })),[ list ]) } </ul> { useMemo(()=> <ChildrenComponent />,[]) } </div>
}
複製代碼
有狀態組件
在class
聲明的組件中,沒有像 useMemo
的API
,可是也並不等於一籌莫展,咱們能夠經過 react.memo
來阻攔來自組件自己的更新。咱們能夠寫一個組件,來控制react
組件更新的方向。咱們經過一個 <NotUpdate>
組件來阻斷更新流。
/* 控制更新 ,第二個參數能夠做爲組件更新的依賴 , 這裏設置爲 ()=> true 只渲染一次 */
const NotUpdate = React.memo(({ children }:any)=> typeof children === 'function' ? children() : children ,()=>true)
class Index extends React.Component<any,any>{
constructor(prop){
super(prop)
this.state = {
list: [ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ],
number:0,
}
}
handerClick = ()=>{
this.setState({ number:this.state.number + 1 })
}
render(){
const { list }:any = this.state
return <div> <button onClick={ this.handerClick } >點擊</button> <NotUpdate> {()=>(<ul> { list.map(item=>{ console.log(1111) return <li key={ item.id } >{ item.name }</li> }) } </ul>)} </NotUpdate> <NotUpdate> <ChildrenComponent /> </NotUpdate> </div>
}
}
複製代碼
const NotUpdate = React.memo(({ children }:any)=> typeof children === 'function' ? children() : children ,()=>true)
複製代碼
沒錯,用的就是 React.memo
,生成了阻斷更新的隔離單元,若是咱們想要控制更新,能夠對 React.memo
第二個參數入手, demo
項目中徹底阻斷的更新。
這裏的取締state
,並徹底不使用state
來管理數據,而是善於使用state
,知道何時使用,怎麼使用。react
並不像 vue
那樣響應式數據流。 在 vue
中 有專門的dep
作依賴收集,能夠自動收集字符串模版的依賴項,只要沒有引用的data
數據, 經過 this.aaa = bbb
,在vue
中是不會更新渲染的。由於 aaa
的dep
沒有收集渲染watcher
依賴項。在react
中,咱們觸發this.setState
或者 useState
,只會關心兩次state
值是否相同,來觸發渲染,根本不會在意jsx
語法中是否真正的引入了正確的值。
有狀態組件中
class Demo extends React.Component{
state={ text:111 }
componentDidMount(){
const { a } = this.props
/* 咱們只是但願在初始化,用text記錄 props中 a 的值 */
this.setState({
text:a
})
}
render(){
/* 沒有引入text */
return <div>{'hello,world'}</div>
}
}
複製代碼
如上例子中,render
函數中並無引入text
,咱們只是但願在初始化的時候,用 text
記錄 props
中 a
的值。咱們卻用 setState
觸發了一次無用的更新。無狀態組件中狀況也同樣存在,具體以下。
無狀態組件中
function Demo ({ a }){
const [text , setText] = useState(111)
useEffect(()=>{
setText(a)
},[])
return <div> {'hello,world'} </div>
}
複製代碼
有狀態組件中
在class
聲明組件中,咱們能夠直接把數據綁定給this
上,來做爲數據緩存。
class Demo extends React.Component{
text = 111
componentDidMount(){
const { a } = this.props
/* 數據直接保存在text上 */
this.text = a
}
render(){
/* 沒有引入text */
return <div>{'hello,world'}</div>
}
}
複製代碼
無狀態組件中
在無狀態組件中, 咱們不能往問this
,可是咱們能夠用useRef
來解決問題。
function Demo ({ a }){
const text = useRef(111)
useEffect(()=>{
text.current = a
},[])
return <div> {'hello,world'} </div>
}
複製代碼
useCallback
的真正目的仍是在於緩存了每次渲染時 inline callback
的實例,這樣方便配合上子組件的 shouldComponentUpdate
或者 React.memo
起到減小沒必要要的渲染的做用。對子組件的渲染限定來源與,對子組件props
比較,可是若是對父組件的callback
作比較,無狀態組件每次渲染執行,都會造成新的callback
,是沒法比較,因此須要對callback
作一個 memoize
記憶功能,咱們能夠理解爲useCallback
就是 callback
加了一個memoize
。咱們接着往下看👇👇👇。
function demo (){
const [ number , setNumber ] = useState(0)
return <div> <DemoComponent handerChange={ ()=>{ setNumber(number+1) } } /> </div>
}
複製代碼
或着
function demo (){
const [ number , setNumber ] = useState(0)
const handerChange = ()=>{
setNumber(number+1)
}
return <div> <DemoComponent handerChange={ handerChange } /> </div>
}
複製代碼
不管是上述那種方式,pureComponent
和 react.memo
經過淺比較方式,只能判斷每次更新都是新的callback
,而後觸發渲染更新。useCallback
給加了一個記憶功能,告訴咱們子組件,兩次是相同的 callback
無需從新更新頁面。至於何時callback
更改,就要取決於 useCallback
第二個參數。好的,將上述demo
咱們用 useCallback
重寫。
function demo (){
const [ number , setNumber ] = useState(0)
const handerChange = useCallback( ()=>{
setNumber(number+1)
},[])
return <div> <DemoComponent handerChange={ handerChange } /> </div>
}
複製代碼
這樣 pureComponent
和 react.memo
能夠直接判斷是callback
沒有改變,防止了沒必要要渲染。
不管咱們使用的是redux
仍是說 redux
衍生出來的 dva
,redux-saga
等,或者是mobx
,都要遵循必定'使用規則',首先讓我想到的是,何時用狀態管理,怎麼合理的應用狀態管理,接下來咱們來分析一下。
要問我何時適合使用狀態狀態管理。我必定會這麼分析,首先狀態管理是爲了解決什麼問題,狀態管理可以解決的問題主要分爲兩個方面,一 就是解決跨層級組件通訊問題 。二 就是對一些全局公共狀態的緩存。
咱們那redux系列的狀態管理爲例子。
我見過又同窗這麼寫的
/* 和 store下面text模塊的list列表,創建起依賴關係,list更新,組件從新渲染 */
@connect((store)=>({ list:store.text.list }))
class Text extends React.Component{
constructor(prop){
super(prop)
}
componentDidMount(){
/* 初始化請求數據 */
this.getList()
}
getList=()=>{
const { dispatch } = this.props
/* 獲取數據 */
dispatch({ type:'text/getDataList' })
}
render(){
const { list } = this.props
return <div> { list.map(item=><div key={ item.id } > { /* 作一些渲染頁面的操做.... */ } </div>) } <button onClick={ ()=>this.getList() } >從新獲取列表</button> </div>
}
}
複製代碼
這樣頁面請求數據,到數據更新,所有在當前組件發生,這個寫法我不推薦,此時的數據走了一遍狀態管理,最終仍是回到了組件自己,顯得很雞肋,並無發揮什麼做用。在性能優化上到不如直接在組件內部請求數據。
還有的同窗可能這麼寫。
class Text extends React.Component{
constructor(prop){
super(prop)
this.state={
list:[],
}
}
async componentDidMount(){
const { data , code } = await getList()
if(code === 200){
/* 獲取的數據有多是不常變的,多個頁面須要的數據 */
this.setState({
list:data
})
}
}
render(){
const { list } = this.state
return <div> { /* 下拉框 */ } <select> { list.map(item=><option key={ item.id } >{ item.name }</option>) } </select> </div>
}
}
複製代碼
對於不變的數據,多個頁面或組件須要的數據,爲了不重複請求,咱們能夠將數據放在狀態管理裏面。
咱們要學會分析頁面,那些數據是不變的,那些是隨時變更的,用如下demo
頁面爲例子:
如上 紅色區域,是基本不變的數據,多個頁面可能須要的數據,咱們能夠統一放在狀態管理中,藍色區域是隨時更新的數據,直接請求接口就好。
不變的數據,多個頁面可能須要的數據,放在狀態管理中,對於時常變化的數據,咱們能夠直接請求接口
時間分片的概念,就是一次性渲染大量數據,初始化的時候會出現卡頓等現象。咱們必需要明白的一個道理,js執行永遠要比dom渲染快的多。 ,因此對於大量的數據,一次性渲染,容易形成卡頓,卡死的狀況。咱們先來看一下例子
class Index extends React.Component<any,any>{
state={
list: []
}
handerClick=()=>{
let starTime = new Date().getTime()
this.setState({
list: new Array(40000).fill(0)
},()=>{
const end = new Date().getTime()
console.log( (end - starTime ) / 1000 + '秒')
})
}
render(){
const { list } = this.state
console.log(list)
return <div> <button onClick={ this.handerClick } >點擊</button> { list.map((item,index)=><li className="list" key={index} > { item + '' + index } Item </li>) } </div>
}
}
複製代碼
咱們模擬一次性渲染 40000 個數據的列表,看一下須要多長時間。
咱們看到 40000 個 簡單列表渲染了,將近5秒的時間。爲了解決一次性加載大量數據的問題。咱們引出了時間分片的概念,就是用setTimeout
把任務分割,分紅若干次來渲染。一共40000個數據,咱們能夠每次渲染100個, 分次400渲染。
class Index extends React.Component<any,any>{
state={
list: []
}
handerClick=()=>{
this.sliceTime(new Array(40000).fill(0), 0)
}
sliceTime=(list,times)=>{
if(times === 400) return
setTimeout(() => {
const newList = list.slice( times , (times + 1) * 100 ) /* 每次截取 100 個 */
this.setState({
list: this.state.list.concat(newList)
})
this.sliceTime( list ,times + 1 )
}, 0)
}
render(){
const { list } = this.state
return <div> <button onClick={ this.handerClick } >點擊</button> { list.map((item,index)=><li className="list" key={index} > { item + '' + index } Item </li>) } </div>
}
}
複製代碼
效果
setTimeout
能夠用 window.requestAnimationFrame()
代替,會有更好的渲染效果。 咱們demo
使用列表作的,實際對於列表來講,最佳方案是虛擬列表,而時間分片,更適合熱力圖,地圖點位比較多的狀況。
筆者在最近在作小程序商城項目,有長列表的狀況, 但是確定說 虛擬列表 是解決長列表渲染的最佳方案。不管是小程序,或者是h5
,隨着 dom
元素愈來愈多,頁面會愈來愈卡頓,這種狀況在小程序更加明顯 。稍後,筆者講專門寫一篇小程序長列表渲染緩存方案的文章,感興趣的同窗能夠關注一下筆者。
虛擬列表是按需顯示的一種技術,能夠根據用戶的滾動,沒必要渲染全部列表項,而只是渲染可視區域內的一部分列表元素的技術。正常的虛擬列表分爲 渲染區,緩衝區 ,虛擬列表區。
以下圖所示。
爲了防止大量dom
存在影響性能,咱們只對,渲染區和緩衝區的數據作渲染,,虛擬列表區 沒有真實的dom存在。 緩衝區的做用就是防止快速下滑或者上滑過程當中,會有空白的現象。
react-tiny-virtual-list 是一個較爲輕量的實現虛擬列表的組件。這是官方文檔。
import React from 'react';
import {render} from 'react-dom';
import VirtualList from 'react-tiny-virtual-list';
const data = ['A', 'B', 'C', 'D', 'E', 'F', ...];
render(
<VirtualList width='100%' height={600} itemCount={data.length} itemSize={50} // Also supports variable heights (array or function getter) renderItem={({index, style}) => <div key={index} style={style}> // The style property contains the item's absolute position Letter: {data[index]}, Row: #{index} </div> } />,
document.getElementById('root')
);
複製代碼
let num = 0
class Index extends React.Component<any, any>{
state = {
list: new Array(9999).fill(0).map(() =>{
num++
return num
}),
scorllBoxHeight: 500, /* 容器高度(初始化高度) */
renderList: [], /* 渲染列表 */
itemHeight: 60, /* 每個列表高度 */
bufferCount: 8, /* 緩衝個數 上下四個 */
renderCount: 0, /* 渲染數量 */
start: 0, /* 起始索引 */
end: 0 /* 終止索引 */
}
listBox: any = null
scrollBox : any = null
scrollContent:any = null
componentDidMount() {
const { itemHeight, bufferCount } = this.state
/* 計算容器高度 */
const scorllBoxHeight = this.listBox.offsetHeight
const renderCount = Math.ceil(scorllBoxHeight / itemHeight) + bufferCount
const end = renderCount + 1
this.setState({
scorllBoxHeight,
end,
renderCount,
})
}
/* 處理滾動效果 */
handerScroll=()=>{
const { scrollTop } :any = this.scrollBox
const { itemHeight , renderCount } = this.state
const currentOffset = scrollTop - (scrollTop % itemHeight)
/* translate3d 開啓css cpu 加速 */
this.scrollContent.style.transform = `translate3d(0, ${currentOffset}px, 0)`
const start = Math.floor(scrollTop / itemHeight)
const end = Math.floor(scrollTop / itemHeight + renderCount + 1)
this.setState({
start,
end,
})
}
/* 性能優化:只有在列表start 和 end 改變的時候在渲染列表 */
shouldComponentUpdate(_nextProps, _nextState){
const { start , end } = _nextState
return start !== this.state.start || end !==this.state.end
}
/* 處理滾動效果 */
render() {
console.log(1111)
const { list, scorllBoxHeight, itemHeight ,start ,end } = this.state
const renderList = list.slice(start,end)
return <div className="list_box" ref={(node) => this.listBox = node} > <div style={{ height: scorllBoxHeight, overflow: 'scroll', position: 'relative' }} ref={ (node)=> this.scrollBox = node } onScroll={ this.handerScroll } > { /* 佔位做用 */} <div style={{ height: `${list.length * itemHeight}px`, position: 'absolute', left: 0, top: 0, right: 0 }} /> { /* 顯然區 */ } <div ref={(node) => this.scrollContent = node} style={{ position: 'relative', left: 0, top: 0, right: 0 }} > { renderList.map((item, index) => ( <div className="list" key={index} > {item + '' } Item </div> )) } </div> </div> </div>
}
}
複製代碼
效果
具體思路
① 初始化計算容器的高度。截取初始化列表長度。這裏咱們須要div佔位,撐起滾動條。
② 經過監聽滾動容器的 onScroll
事件,根據 scrollTop
來計算渲染區域向上偏移量, 咱們要注意的是,當咱們向下滑動的時候,爲了渲染區域,能在可視區域內,可視區域要向上的滾動; 咱們向上滑動的時候,可視區域要向下的滾動。
③ 經過從新計算的 end
和 start
來從新渲染列表。
性能優化點
① 對於移動視圖區域,咱們能夠用 transform
來代替改變 top
值。
② 虛擬列表實際狀況,是有 start
或者 end
改變的時候,在從新渲染列表,因此咱們能夠用以前 shouldComponentUpdate
來調優,避免重複渲染。
react
性能優化是一個攻堅戰,須要付出不少努力,將咱們的項目作的更完美,但願看完這片文章的朋友們能找到react
優化的方向,讓咱們的react
項目飛起來。
感受有用的朋友能夠關注筆者公衆號 前端Sharing 持續更新好文章。