性能優(yōu)化小冊(cè) - React 搜索優(yōu)化:防抖、緩存、LRU
最近要主導(dǎo) react 項(xiàng)目重構(gòu)優(yōu)化等相關(guān)的工作,由于有好長(zhǎng)時(shí)間沒(méi)碰 React 了,今天索性把一個(gè)基于關(guān)鍵字搜索的 demo 做一下簡(jiǎn)單優(yōu)化,在此記錄一下。
主要從三個(gè)方面進(jìn)行優(yōu)化處理:
減少事件的觸發(fā)頻率 - 對(duì)關(guān)鍵字鍵入進(jìn)行? debounce?處理減少 HTTP 請(qǐng)求 - 對(duì)重復(fù)的 HTTP 請(qǐng)求進(jìn)行緩存攔截 緩存淘汰策略 - 使用 LRU 優(yōu)化緩存
減少事件的觸發(fā)頻率 - debounce
debounce?旨在時(shí)間段內(nèi)控制事件只在最后一次操作觸發(fā)。
debounce?原理:是維護(hù)一個(gè)計(jì)時(shí)器,在規(guī)定的?delay?時(shí)間后觸發(fā)函數(shù),在?delay?時(shí)間內(nèi)再次觸發(fā)的話,就會(huì)取消之前的計(jì)時(shí)器而重新設(shè)置。這樣一來(lái),只有最后一次操作能被觸發(fā)。
下面是 react 中?debounce?優(yōu)化的代碼:
...
handler?=?e?=>?{
??let?val?=?e.target.value;
??if(val)?{?
????this.search(val);
??}
??this.setState(()?=>?({
????value:?e.target.value
??}))
}
debounce?=?(fn,?delay)?=>?{
??let?timer?=?null;
??return?function(event)?{
????timer?&&?clearTimeout(timer);
????event.persist?&&?event.persist()?//?保留引用,已備異步階段訪問(wèn)
????timer?=?setTimeout(()?=>?{
??????fn.call(this,?event)
????},?delay)?
??}
}
onChangeHandler?=?this.debounce(this.handler,?1000)
...
render()?{
??return?(
????<div>
??????<input
????????//?這里不能設(shè)置成?value
????????defaulValue={this.state.value}
????????onChange={e?=>?this.onChangeHandler(e)}
????????placeholder="試著輸入一些文字"
??????/>
??????<div>
????????<Suspense?fallback="Loading">
??????????{this.renderMovies}
????????Suspense>
??????div>
????div>
??);
}
這里需要注意的是:?如果想要異步訪問(wèn)合成事件對(duì)象 SyntheticEvent,需要調(diào)用?persist()?方法或者對(duì)事件對(duì)象進(jìn)行深拷貝?const event = { ...event }?保留對(duì)事件的引用。
在 React 事件調(diào)用時(shí),React 傳遞給事件處理程序是一個(gè)合成事件對(duì)象的實(shí)例 SyntheticEvent 是通過(guò)合并得到的。這意味著在事件回調(diào)被調(diào)用后,SyntheticEvent 對(duì)象將被重用并且所有屬性都將被取消。這是出于性能原因,因此,您無(wú)法以異步方式訪問(wèn)該事件。React合成事件官方文檔
event.persist()
//?or
const?event:?SyntheticEvent?=?{?...event?}
還有一個(gè)隱晦點(diǎn)的需要指出,?我們知道如果想要使?input?為受控元素,正確的做法是:在給?input?綁定?value?時(shí),需要同時(shí)綁定?onChange?事件來(lái)監(jiān)聽(tīng)數(shù)據(jù)變化,否則就會(huì)報(bào)如下警告。
但是當(dāng)你異步傳遞?SyntheticEvent?對(duì)象時(shí),使用?value?屬性進(jìn)行綁定的?input,值不會(huì)再發(fā)生變化(但它仍是一個(gè)受控元素)。
...
event.persist()
timer?=?setTimeout(()?=>?{
??fn.call(this,?event)?//?傳遞?event
},?delay)?
...
??defaultValue={this.state.value}
??//?value={this.state.value}?使用?value?屬性,值不會(huì)發(fā)生變化
??onChange={e?=>?this.onChangeHandler(e)}
/>
如下圖:
減少 HTTP 請(qǐng)求
減少 HTTP 請(qǐng)求的手段之一就是將 HTTP 請(qǐng)求結(jié)果進(jìn)行緩存,如果下次請(qǐng)求的?url?未發(fā)生變化,則直接從緩存中獲取數(shù)據(jù)。
import?axios?from?'axios';
const?caches?=?{};?
const?axiosRequester?=?()?=>?{
??let?cancel;
??return?async?url?=>?{
????if(cancel)?{
??????cancel.cancel();
????}
????cancel?=?axios.CancelToken.source();
????try?{
??????if(caches[url])?{?//如果請(qǐng)求的?url?之前已經(jīng)提交過(guò),就不在進(jìn)行請(qǐng)求,返回之前請(qǐng)求回來(lái)的數(shù)據(jù)
????????return?caches[url];
??????}
??????const?res?=?await?axios.post(url,?{
?????????cancelToken:?cancel.token
??????})
??????const?result?=?res.data.result;
??????caches[url]?=?result;??//將?url作為?key,?result?為請(qǐng)求回來(lái)的數(shù)據(jù),存儲(chǔ)起來(lái)
??????return?result;
????}?catch(error)?{
??????if(axios.isCancel(error))?{
????????console.log('Request?canceled',?error.message);
??????}?else?{
????????console.log(error.message);
??????}
????}
??}
}
export?const?_search?=?axiosRequester();
在使用?axios?進(jìn)行 HTTP 請(qǐng)求時(shí),首先根據(jù)?url?判斷數(shù)據(jù)是否已被緩存,如果命中則直接從緩存中拿數(shù)據(jù)。如果未被緩存,則發(fā)起?HTTP?請(qǐng)求,并將請(qǐng)求回來(lái)的結(jié)果以鍵值對(duì)的形式保存在?caches?對(duì)象中。
緩存淘汰策略 - LRU
由于緩存空間是有限的,所以不能無(wú)限制的進(jìn)行數(shù)據(jù)存儲(chǔ),當(dāng)存儲(chǔ)容量達(dá)到一個(gè)閥值時(shí),就會(huì)造成內(nèi)存溢出,因此在進(jìn)行數(shù)據(jù)緩存時(shí),就要根據(jù)情況對(duì)緩存進(jìn)行優(yōu)化,清除一些可能不會(huì)再用到的數(shù)據(jù)。
這里我們用到 keepAlive 相同的緩存淘汰機(jī)制 - LRU。
LRU - 最近最少使用策略
以時(shí)間作為參考,如果數(shù)據(jù)最近被訪問(wèn)過(guò),那么將來(lái)被訪問(wèn)的幾率會(huì)更高,如果以一個(gè)數(shù)組去記錄數(shù)據(jù),當(dāng)有一數(shù)據(jù)被訪問(wèn)時(shí),該數(shù)據(jù)會(huì)被移動(dòng)到數(shù)組的末尾,表明最近被使用過(guò),當(dāng)緩存溢出時(shí),會(huì)刪除數(shù)組的頭部數(shù)據(jù),即將最不頻繁使用的數(shù)據(jù)移除。
實(shí)現(xiàn) LRU 策略我們需要一個(gè)存儲(chǔ)緩存對(duì)象?key?的數(shù)組:
const?keys?=?[];
并且需要設(shè)置一個(gè)閥值,控制緩存棧最大的存儲(chǔ)數(shù)量:
const?MAXIMUN_CACHES?=?20;
還需要一個(gè)用來(lái)刪除數(shù)組?keys?成員項(xiàng)的工具函數(shù)?remove:
function?remove(arr,?item)?{
??if?(arr.length)?{
????var?index?=?arr.indexOf(item)
????if?(index?>?-1)?{
??????return?arr.splice(index,?1)
????}
??}
}
最后再實(shí)現(xiàn)一個(gè)?pruneCacheEntry?函數(shù),用來(lái)刪除最少訪問(wèn)的數(shù)據(jù)(第一項(xiàng)):
//?傳入?keys?數(shù)組的第一項(xiàng)
if?(keys.length?>?parseInt(MAXIMUN_CACHES))?{
??pruneCacheEntry(caches,?keys[0],?keys);
}
...
//?刪除最少訪問(wèn)的數(shù)據(jù)
function?pruneCacheEntry?(?caches,?key,?keys)?{
??caches[key]?=?null;?//?清空對(duì)應(yīng)的數(shù)據(jù)
??delete?caches[key];?//?刪除緩存?key
??remove(keys,?key);
}
最終「鍵入防抖」結(jié)合 LRU 緩存優(yōu)化后的搜索功能就像這樣:

同系列文章:
性能優(yōu)化小冊(cè) - 異步堆棧追蹤:為什么 await 勝過(guò) Promise 性能優(yōu)化小冊(cè) - 分類構(gòu)建:利用好 webpack hash 性能優(yōu)化小冊(cè) - 提高網(wǎng)頁(yè)響應(yīng)速度:優(yōu)化你的 CDN 性能 性能優(yōu)化小冊(cè) - 可編程式緩存:Service Workers 性能優(yōu)化小冊(cè) - 讓頁(yè)面更早的渲染:使用 preload 提升資源加載優(yōu)先級(jí)


