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

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


