作者 | 邱俊濤
性能問題是軟件開發(fā)中的常見問題,我們在幾乎每個項(xiàng)目在某個時期(往往是在后期快要交付的時候,或者已經(jīng)上線以后收到用戶反饋)都或多或少會遇到。這篇文章想要從流程方面和具體的技術(shù)細(xì)節(jié)上對軟件性能優(yōu)化上遇到的問題做一些總結(jié)和分類,以方便在后續(xù)類似的場景下可以提供給開發(fā)者一個參考。
嚴(yán)格意義上,這篇文章并沒有太多的新內(nèi)容,甚至有一些具體的技術(shù)細(xì)節(jié)我在另一篇文章中已經(jīng)討論過,這里主要還是提供一些常見的關(guān)于性能優(yōu)化思路的總結(jié)。

在修改之前
性能優(yōu)化之法,曰立,曰測,曰理,曰拆,曰分,曰剝,曰拖,曰緩。
我們討論性能提升,往往需要首先建立一套測量機(jī)制。因?yàn)閮H憑直覺來猜測可能的性能瓶頸非常低效,而且往往直覺認(rèn)為有性能問題的地方未必真有問題。一旦測量機(jī)制建立,則猶如我們代碼有了單元測試/集成測試的守護(hù),總的來說會向著正確的方向演進(jìn)。
立字訣
重中之重的是,定義好指標(biāo)。即DoD(Definition of Done),我們需要回答的問題是:什么是好的性能?達(dá)到何種標(biāo)準(zhǔn)就算是提升,而達(dá)不到就算是失敗?這一點(diǎn)從項(xiàng)目的確立角度非常關(guān)鍵。
如果說希望某個頁面的性能較之以前來說,加載時間提高20%為成功,則一切的后續(xù)開發(fā)可以做到有的放矢,而不至于無疾而終。
測字訣
一旦我們定義好了何為提升。接下來就需要建立相應(yīng)的測量機(jī)制,并設(shè)置基線。這一步相當(dāng)于將上一步定義好的標(biāo)準(zhǔn)實(shí)例化到build pipeline中,使得具體目標(biāo)可視化起來,從而每次的修改都能看到和目標(biāo)的差距。
比如從請求發(fā)出到頁面渲染完成(比如檢測到某個標(biāo)的在頁面上的存在與否),總共耗時3秒,然后我們將3秒設(shè)置為基線,并圍繞這個基線設(shè)置測試的上限。和其他測試一樣,如果后續(xù)的代碼修改使得頁面渲染時間大于基線值,則build失敗。與之對應(yīng)的還可以有諸如bundle的尺寸(壓縮后的靜態(tài)資源大小)首次渲染時間等等指標(biāo)。
有了具體的目標(biāo),我們就可以設(shè)置相應(yīng)的測試機(jī)制。比如通過運(yùn)行yslow或者其他lighthouse來進(jìn)行。
理字訣
當(dāng)我們定義了性能優(yōu)化成功的含義,也有了相應(yīng)的反饋機(jī)制,如何做才會成為最重要的主題。對于這個問題,常用的工具就是分析和分類。
首先需要的分析“慢”的類型,是純性能問題,還是架構(gòu)問題,或者是軟件設(shè)計(jì)上的問題。純性能的問題往往較為具體,也最容易解決,比如使用了性能較低的包作為依賴,則只需要替換為性能更好的庫即可;又或者使用debounce/throttle來減少對函數(shù)的頻繁調(diào)用等等。
與純粹的性能問題相對應(yīng)的另一大類問題,都可以歸結(jié)到設(shè)計(jì)問題(大到軟件架構(gòu),小到模塊間的耦合/依賴等問題)。這類問題通常需要引入的修改比較大,但是收益也會很高,而且長期來看,對于代碼的可維護(hù)性和缺陷率也會帶來好的回報。
因此,這一步的目標(biāo)是識別出哪些問題可以通過簡單修改就可以達(dá)成,而另外的一些則需要大的改動。事實(shí)是,有可能對于我們之前定義好的基線,只需要解決純粹的性能問題就可以達(dá)成,那我們也無需花費(fèi)大量的工作在更大的修改上。
總綱
或曰,性能優(yōu)化之訣竅,唯推拖二字也。推者,不是我的事兒我絕不干,誰愛干誰干。拖者,能明天做的事兒,今天絕不去碰。
如果純粹的最佳實(shí)踐無法滿足要求,我們則需要花費(fèi)更多的時間來重構(gòu)代碼的設(shè)計(jì)來滿足性能需求。
我們將通過一些具體的例子來仔細(xì)討論。總的來說,我們需要識別代碼中的耦合問題,并在合理的方向上進(jìn)行抽象,并完成拆分,使得每個獨(dú)立的模塊/組件都盡可能的高內(nèi)聚,低耦合。
拆字訣
比如在文中討論的Avatar和Tooltip的例子,頭像組件Avartar的核心功能并不包含Tooltip,而且兩者的耦合程度其實(shí)很低,可以通過拆分的方式將其隔離。
修改后的Avatar不再將Tooltip做為依賴:

分字訣
在另外一些情況下,一個組件和其依賴間的耦合較為緊密,但是又不具備不可替代性。比如在文中討論的InlineEdit和InlineDialog的場景。
這時候可以通過render props來進(jìn)行控制反轉(zhuǎn),使得組件不再依賴于某個具體實(shí)現(xiàn),而是一個接口。這樣所有實(shí)現(xiàn)了該接口的組件都可以即插即用,又可以節(jié)省默認(rèn)依賴的部分開銷(定義在package.json中的)。
注意這種場景和“拆字訣”里的場景非常類似,不過區(qū)別是這里拆分出去的組件和當(dāng)前組件間有一個隱式的協(xié)定:即需要接受render傳遞過去的所有參數(shù)。

比如上面的例子中,editView并不是完全自由定義的,它需要或者接受或者忽略isInvalid和error這樣的參數(shù)。
剝字訣
在一些場景中,與其提供一個大而全的組件,我們可以將該組件適度的附加功能剝離,并形成不同的組件,通過不同的entry-points導(dǎo)出。這樣用戶可以按需安裝。一個典型的例子是lodash的早起版本,用戶如果需要使用partition,仍然需要導(dǎo)入整個包:
通過不同的entry-point,你可以僅僅導(dǎo)入你需要的函數(shù):
類似的,比如你的button組件,你可以提供標(biāo)準(zhǔn)button,加載中的button,或者高級button等不同類型,以便用戶按需使用。
拖字訣
以React為例,我們既可以使用原生的React.lazy也可以使用諸如loadable之類的庫來實(shí)現(xiàn)按需加載。即不到最后一刻(需要渲染DOM的時候)絕不加載。這在很多場景下,特別是提升頁面初始頁的時候非常有用。比如首頁上的User Profile里隱藏著一個巨大的DropdownMenu,我們完全可以當(dāng)用戶第一次使用時再加載。

并提供一個placeholder在加載時:

緩字訣
我們這里介紹的最后一種方法是緩存,將耗時的,且不會頻繁發(fā)生變化的計(jì)算結(jié)果保存起來,以提高后續(xù)的訪問速度。這個模式既可以是代碼層級,將數(shù)據(jù)存放在內(nèi)存中或者LocalStorage/SessionStorage中。另一方面,這條原則從架構(gòu)層面也是適用的,比如我們引入靜態(tài)資源存儲在CDN上,動態(tài)資源存儲于緩存服務(wù)器等。
還是以React為例,我們可以使用:
- 使用useMemo緩存數(shù)據(jù)
- 使用useCallback緩存事件響應(yīng)函數(shù)
- 使用memo對靜態(tài)組件(特別是葉子節(jié)點(diǎn))進(jìn)行緩存
比如對于一個葉子節(jié)點(diǎn)Toggle

使用API級別的緩存之后,寫起來可能是這樣的:
另外應(yīng)該注意的是,使用額外的API如useMemo或者useCallback本身也是有消耗的,在實(shí)際場景里需要結(jié)合上面提到的測字訣來確保實(shí)際數(shù)字上的改善,而不是對迷信API。
小結(jié)
本文對性能優(yōu)化中常見的一些方法和模式做了一些總結(jié)。在開始實(shí)施之前,我們需要確定對性能優(yōu)化成功與否的定義。然后我們需要設(shè)立基線以及與之匹配的測試,這樣我們在任何時候都可以確知我們的優(yōu)化有沒有效果,或者與預(yù)期之間的差距,從而時刻保證目標(biāo)清晰。接下來需要對性能問題的現(xiàn)象進(jìn)行初步的分析和分類,比如是架構(gòu)上的缺陷,或者是微觀代碼層面沒有采用最佳實(shí)踐等。
接下來,我們討論了幾類常見的優(yōu)化方法。比如根據(jù)耦合度的拆分,根據(jù)復(fù)雜/分化程度的拆分,使用接口來實(shí)現(xiàn)依賴倒置,以及緩存的使用等。這些具體的做法在不同的技術(shù)棧上可能有不同的具體實(shí)踐(比如Angular中可能有l(wèi)azy的對照物,或者可以在vue中采用類似的技術(shù)來實(shí)現(xiàn)memo等),但是這些思路是比較通用的,可以應(yīng)用在類似的場景。
原文鏈接:??前端性能優(yōu)化心法 (qq.com)??






























