作者 | 邱俊濤
性能問(wèn)題是軟件開(kāi)發(fā)中的常見(jiàn)問(wèn)題,我們?cè)趲缀趺總€(gè)項(xiàng)目在某個(gè)時(shí)期(往往是在后期快要交付的時(shí)候,或者已經(jīng)上線以后收到用戶反饋)都或多或少會(huì)遇到。這篇文章想要從流程方面和具體的技術(shù)細(xì)節(jié)上對(duì)軟件性能優(yōu)化上遇到的問(wèn)題做一些總結(jié)和分類,以方便在后續(xù)類似的場(chǎng)景下可以提供給開(kāi)發(fā)者一個(gè)參考。
嚴(yán)格意義上,這篇文章并沒(méi)有太多的新內(nèi)容,甚至有一些具體的技術(shù)細(xì)節(jié)我在另一篇文章中已經(jīng)討論過(guò),這里主要還是提供一些常見(jiàn)的關(guān)于性能優(yōu)化思路的總結(jié)。
在修改之前
性能優(yōu)化之法,曰立,曰測(cè),曰理,曰拆,曰分,曰剝,曰拖,曰緩。
我們討論性能提升,往往需要首先建立一套測(cè)量機(jī)制。因?yàn)閮H憑直覺(jué)來(lái)猜測(cè)可能的性能瓶頸非常低效,而且往往直覺(jué)認(rèn)為有性能問(wèn)題的地方未必真有問(wèn)題。一旦測(cè)量機(jī)制建立,則猶如我們代碼有了單元測(cè)試/集成測(cè)試的守護(hù),總的來(lái)說(shuō)會(huì)向著正確的方向演進(jìn)。
立字訣
重中之重的是,定義好指標(biāo)。即DoD(Definition of Done),我們需要回答的問(wèn)題是:什么是好的性能?達(dá)到何種標(biāo)準(zhǔn)就算是提升,而達(dá)不到就算是失?。窟@一點(diǎn)從項(xiàng)目的確立角度非常關(guān)鍵。
如果說(shuō)希望某個(gè)頁(yè)面的性能較之以前來(lái)說(shuō),加載時(shí)間提高20%為成功,則一切的后續(xù)開(kāi)發(fā)可以做到有的放矢,而不至于無(wú)疾而終。
測(cè)字訣
一旦我們定義好了何為提升。接下來(lái)就需要建立相應(yīng)的測(cè)量機(jī)制,并設(shè)置基線。這一步相當(dāng)于將上一步定義好的標(biāo)準(zhǔn)實(shí)例化到build pipeline中,使得具體目標(biāo)可視化起來(lái),從而每次的修改都能看到和目標(biāo)的差距。
比如從請(qǐng)求發(fā)出到頁(yè)面渲染完成(比如檢測(cè)到某個(gè)標(biāo)的在頁(yè)面上的存在與否),總共耗時(shí)3秒,然后我們將3秒設(shè)置為基線,并圍繞這個(gè)基線設(shè)置測(cè)試的上限。和其他測(cè)試一樣,如果后續(xù)的代碼修改使得頁(yè)面渲染時(shí)間大于基線值,則build失敗。與之對(duì)應(yīng)的還可以有諸如bundle的尺寸(壓縮后的靜態(tài)資源大小)首次渲染時(shí)間等等指標(biāo)。
有了具體的目標(biāo),我們就可以設(shè)置相應(yīng)的測(cè)試機(jī)制。比如通過(guò)運(yùn)行yslow或者其他lighthouse來(lái)進(jìn)行。
理字訣
當(dāng)我們定義了性能優(yōu)化成功的含義,也有了相應(yīng)的反饋機(jī)制,如何做才會(huì)成為最重要的主題。對(duì)于這個(gè)問(wèn)題,常用的工具就是分析和分類。
首先需要的分析“慢”的類型,是純性能問(wèn)題,還是架構(gòu)問(wèn)題,或者是軟件設(shè)計(jì)上的問(wèn)題。純性能的問(wèn)題往往較為具體,也最容易解決,比如使用了性能較低的包作為依賴,則只需要替換為性能更好的庫(kù)即可;又或者使用debounce/throttle來(lái)減少對(duì)函數(shù)的頻繁調(diào)用等等。
與純粹的性能問(wèn)題相對(duì)應(yīng)的另一大類問(wèn)題,都可以歸結(jié)到設(shè)計(jì)問(wèn)題(大到軟件架構(gòu),小到模塊間的耦合/依賴等問(wèn)題)。這類問(wèn)題通常需要引入的修改比較大,但是收益也會(huì)很高,而且長(zhǎng)期來(lái)看,對(duì)于代碼的可維護(hù)性和缺陷率也會(huì)帶來(lái)好的回報(bào)。
因此,這一步的目標(biāo)是識(shí)別出哪些問(wèn)題可以通過(guò)簡(jiǎn)單修改就可以達(dá)成,而另外的一些則需要大的改動(dòng)。事實(shí)是,有可能對(duì)于我們之前定義好的基線,只需要解決純粹的性能問(wèn)題就可以達(dá)成,那我們也無(wú)需花費(fèi)大量的工作在更大的修改上。
總綱
或曰,性能優(yōu)化之訣竅,唯推拖二字也。推者,不是我的事兒我絕不干,誰(shuí)愛(ài)干誰(shuí)干。拖者,能明天做的事兒,今天絕不去碰。
如果純粹的最佳實(shí)踐無(wú)法滿足要求,我們則需要花費(fèi)更多的時(shí)間來(lái)重構(gòu)代碼的設(shè)計(jì)來(lái)滿足性能需求。
我們將通過(guò)一些具體的例子來(lái)仔細(xì)討論??偟膩?lái)說(shuō),我們需要識(shí)別代碼中的耦合問(wèn)題,并在合理的方向上進(jìn)行抽象,并完成拆分,使得每個(gè)獨(dú)立的模塊/組件都盡可能的高內(nèi)聚,低耦合。
拆字訣
比如在文中討論的Avatar和Tooltip的例子,頭像組件Avartar的核心功能并不包含Tooltip,而且兩者的耦合程度其實(shí)很低,可以通過(guò)拆分的方式將其隔離。
修改后的Avatar不再將Tooltip做為依賴:
分字訣
在另外一些情況下,一個(gè)組件和其依賴間的耦合較為緊密,但是又不具備不可替代性。比如在文中討論的InlineEdit和InlineDialog的場(chǎng)景。
這時(shí)候可以通過(guò)render props來(lái)進(jìn)行控制反轉(zhuǎn),使得組件不再依賴于某個(gè)具體實(shí)現(xiàn),而是一個(gè)接口。這樣所有實(shí)現(xiàn)了該接口的組件都可以即插即用,又可以節(jié)省默認(rèn)依賴的部分開(kāi)銷(定義在package.json中的)。
注意這種場(chǎng)景和“拆字訣”里的場(chǎng)景非常類似,不過(guò)區(qū)別是這里拆分出去的組件和當(dāng)前組件間有一個(gè)隱式的協(xié)定:即需要接受render傳遞過(guò)去的所有參數(shù)。
比如上面的例子中,editView并不是完全自由定義的,它需要或者接受或者忽略isInvalid和error這樣的參數(shù)。
剝字訣
在一些場(chǎng)景中,與其提供一個(gè)大而全的組件,我們可以將該組件適度的附加功能剝離,并形成不同的組件,通過(guò)不同的entry-points導(dǎo)出。這樣用戶可以按需安裝。一個(gè)典型的例子是lodash的早起版本,用戶如果需要使用partition,仍然需要導(dǎo)入整個(gè)包:
通過(guò)不同的entry-point,你可以僅僅導(dǎo)入你需要的函數(shù):
類似的,比如你的button組件,你可以提供標(biāo)準(zhǔn)button,加載中的button,或者高級(jí)button等不同類型,以便用戶按需使用。
拖字訣
以React為例,我們既可以使用原生的React.lazy也可以使用諸如loadable之類的庫(kù)來(lái)實(shí)現(xiàn)按需加載。即不到最后一刻(需要渲染DOM的時(shí)候)絕不加載。這在很多場(chǎng)景下,特別是提升頁(yè)面初始頁(yè)的時(shí)候非常有用。比如首頁(yè)上的User Profile里隱藏著一個(gè)巨大的DropdownMenu,我們完全可以當(dāng)用戶第一次使用時(shí)再加載。
并提供一個(gè)placeholder在加載時(shí):
緩字訣
我們這里介紹的最后一種方法是緩存,將耗時(shí)的,且不會(huì)頻繁發(fā)生變化的計(jì)算結(jié)果保存起來(lái),以提高后續(xù)的訪問(wèn)速度。這個(gè)模式既可以是代碼層級(jí),將數(shù)據(jù)存放在內(nèi)存中或者LocalStorage/SessionStorage中。另一方面,這條原則從架構(gòu)層面也是適用的,比如我們引入靜態(tài)資源存儲(chǔ)在CDN上,動(dòng)態(tài)資源存儲(chǔ)于緩存服務(wù)器等。
還是以React為例,我們可以使用:
- 使用useMemo緩存數(shù)據(jù)
- 使用useCallback緩存事件響應(yīng)函數(shù)
- 使用memo對(duì)靜態(tài)組件(特別是葉子節(jié)點(diǎn))進(jìn)行緩存
比如對(duì)于一個(gè)葉子節(jié)點(diǎn)Toggle
使用API級(jí)別的緩存之后,寫起來(lái)可能是這樣的:
另外應(yīng)該注意的是,使用額外的API如useMemo或者useCallback本身也是有消耗的,在實(shí)際場(chǎng)景里需要結(jié)合上面提到的測(cè)字訣來(lái)確保實(shí)際數(shù)字上的改善,而不是對(duì)迷信API。
小結(jié)
本文對(duì)性能優(yōu)化中常見(jiàn)的一些方法和模式做了一些總結(jié)。在開(kāi)始實(shí)施之前,我們需要確定對(duì)性能優(yōu)化成功與否的定義。然后我們需要設(shè)立基線以及與之匹配的測(cè)試,這樣我們?cè)谌魏螘r(shí)候都可以確知我們的優(yōu)化有沒(méi)有效果,或者與預(yù)期之間的差距,從而時(shí)刻保證目標(biāo)清晰。接下來(lái)需要對(duì)性能問(wèn)題的現(xiàn)象進(jìn)行初步的分析和分類,比如是架構(gòu)上的缺陷,或者是微觀代碼層面沒(méi)有采用最佳實(shí)踐等。
接下來(lái),我們討論了幾類常見(jiàn)的優(yōu)化方法。比如根據(jù)耦合度的拆分,根據(jù)復(fù)雜/分化程度的拆分,使用接口來(lái)實(shí)現(xiàn)依賴倒置,以及緩存的使用等。這些具體的做法在不同的技術(shù)棧上可能有不同的具體實(shí)踐(比如Angular中可能有l(wèi)azy的對(duì)照物,或者可以在vue中采用類似的技術(shù)來(lái)實(shí)現(xiàn)memo等),但是這些思路是比較通用的,可以應(yīng)用在類似的場(chǎng)景。
原文鏈接:??前端性能優(yōu)化心法 (qq.com)??