我發(fā)現(xiàn)了Vue.js中的性能陷阱
我內心深處對游戲的熱愛,讓我一直渴望能自己制作一些電子游戲。幾個月前我開始將這種夢想變?yōu)楝F(xiàn)實,并第一次參加了全球游戲大賽(Global Game Jam)。我和我的團隊使用 Vue.js 構建了一個名為“ZeroDaysLeft”的游戲:
https://zerodaysleft.netlify.com/
其形式是 Web 端的單頁面應用程序。這款游戲的主題是環(huán)境保護,我們考慮到商業(yè)活動對地球環(huán)境的影響,希望就這個話題做一些有益的探討。使用 Vue.js 制作的游戲并不多。我的團隊遲到了一天,然后用猜拳的方式選擇了我們要用的框架;我們飛快地寫完了代碼,并在周末結束時做出了游戲的可運行版本。在本地測試時一切都很順利。自然,我們?yōu)樽约旱谝淮螌懗鰜淼挠螒蜃髌犯械阶院溃⑾Mc世界分享它。
可是問題出現(xiàn)了——當我們構建好應用并開始查詢域時,內存占用爆表了。它幾乎沒法正常運行,不管換什么機器都會卡住不動,即使在強大的基于 Intel i7 處理器的系統(tǒng)上程序也會崩潰。游戲大賽的時間限制把我們拉回了現(xiàn)實,我們決定擱置生產(chǎn)性能問題,這樣起碼我們能做出一款能在自己的設備上運行的完整游戲。就像大部分的“已完成”項目一樣,第二天我們就把它拋在腦后了。
但我自己沒法釋懷。它一直困擾著我。問題是出在 Vue.js 上嗎?是 Netlify 嗎?還是因為我們的取巧代碼?我必須找出答案。
調查性能下降的原因
我首先使用 Lighthouse 進行了快速測試。所幸 Firefox 為此提供了一個瀏覽器插件:
https://addons.mozilla.org/en-US/firefox/addon/google-lighthouse/
下面就是我得到的結果。
89%的數(shù)字挺不錯的。實際上,與許多流行的網(wǎng)站相比,這個表現(xiàn)相當出色。這個測試指出了一些潛在問題,例如速度指數(shù)和第一次有意義且有內容的繪制步驟等。從理論上講,解決這些問題會進一步提高分數(shù),但不一定能解決應用面臨的嚴重性能問題。
我們的游戲中有一些圖像和音頻素材資源,但是兩者都不至于讓游戲卡死在那里。我們也可以對這些已經(jīng)優(yōu)化過的資源再過度優(yōu)化一遍,但這可能根本就無濟于事。
這個測試無法讓我們真正找出可能導致這一性能問題的原因。于是我開始想:“該不會是 Vue 的問題吧?”這種想法會冒出來也沒什么理由,但要是不檢查一下就是蠢了。我檢查了已部署站點的控制臺,結果空白一片。但警告往往不會在生產(chǎn)中顯示。當我在本地進行相同操作時,一堆 Vue 警告讓我吃了一驚。
像大多數(shù)開發(fā)人員一樣,我對控制臺警告沒那么在意,覺得它們只是警告,而不代表錯誤;所以我一般會把注意力集中在其他地方?;蛟S消除這些警告可以解決我的生產(chǎn)問題,我決定深入研究每個問題并修復它們。
所有這些警告均來自我創(chuàng)建的、用來顯示名為 Cards.vue 選項的組件,因此這個組件可能需要大量重寫。
我決定按順序解決這些控制臺警告。
- > [Vue warn]: Avoid using non-primitive value as key, use string/number value instead.
- found in
- ---> <Cards> at src/components/Cards.vue
Vue.js 有很多指令,讓我們能更直觀地使用框架,比如說 v-for 就可以快速將數(shù)組渲染為列表。使用它時,我們需要一個 :key 才能有效地重渲染組件。但我們將一個對象用作了一個鍵,這是非原始值,因此導致了這個錯誤。我決定將 index.description 用作一個新鍵,因為它是一個字符串,并且在值發(fā)生更改時可以更好地重新渲染。
- > [Vue warn]: Duplicate keys detected: '[object Object]'. This may cause an update error.
- found in
- ---> <Cards> at src/components/Cards.vue
將 :key 更改為一個字符串(index.description)來解決上一個錯誤,就能解決這個重復鍵的錯誤。我們只能將字符串類型寫入 DOM,因此當我們傳遞一個要渲染的對象時,該對象將轉換為等效的字符串(即 [object Object]);并且因為這以前是我們的鍵,所以每個對象都將轉換為 [object Object](除非對象有不同的值),進而會出現(xiàn)重復鍵警告?,F(xiàn)在既然鍵不是對象,警告就會消失,效率也會提升。
- > [Vue warn]: You may have an infinite update loop in a component render function.
- found in
- ---> <Cards> at src/components/Cards.vue
就一個非常模糊的警告來說,這個警告似乎是最重要的:無限循環(huán)意味著內存消耗。這條消息并沒有告訴我們可能出了什么問題,但它確實暗示了問題與組件中的 render 函數(shù)有關。也許是因為我們寫的代碼比較取巧,因此觸發(fā)了不間斷的更新,并占用了大量的計算能力,以至于使瀏覽器和設備崩潰。
這條警告至少告訴我們要檢查 Cards.vue,所以我的第一個想法是檢查組件中的反應屬性,因為這可能會導致錯誤。反應屬性在更改后會觸發(fā)重新渲染。
我們正在顯示 index.days 和 index.description 中的數(shù)據(jù)。但我們不會更改這些數(shù)據(jù),我們從 cardInfo 數(shù)組獲得 index。
- > v-for="index in cardInfo.sort(() => Math.random() - 0.7).slice(0,4)"
我們使用這段代碼對數(shù)組中的元素進行隨機排序,然后將前四個元素顯示為玩家選擇的選項。當用戶單擊一個選項時將調用 effects() 函數(shù),它除了會計算一個動作如何影響游戲狀態(tài)外,還使用 cardInfo 上的拼接原型刪除前四個元素。
在 Vue 這種使用虛擬 DOM 的框架里,用上諸如 cardInfo 之類的反應屬性后,每當數(shù)據(jù)屬性的值更改時都會觸發(fā)重新渲染。在我們的應用里,我們會直接使用 sort() 原型來更改它,然后刪除元素來重新排序。所有這些都會觸發(fā)“無限”的重新渲染,從而引發(fā)警告。
我決定更改數(shù)據(jù)過濾的邏輯,并停止對反應屬性 cardInfo 的多次更改。我安裝了 lodash.shuffle 并定義了一個計算屬性 shuffledList(),它將創(chuàng)建一個名為 list 的 cardInfo 副本。我對其應用了隨機排序操作,并返回了一個“frozen”結果,然后拆分開來顯示四張卡片。我們使用了 Object.freeze(),它將使我們返回的對象不可變,從而完全停止了所有重新渲染操作。
至此,問題解決了。
掉進框架的坑
老實說,當我剛開始調查性能下降原因的時候,還覺得我肯定要優(yōu)化很多資源才能解決問題。最后這個結果說明,在使用許多框架抽象時我們都必須非常小心——特別是在 Vue 中更是如此,只有在必要時才使用某條指令,而且用法一定不能出錯,因為它們絕對有自己的代價。
這還讓我開始思考自己做過的其他工作,其中應用程序可能會因為框架而出現(xiàn)不必要的性能問題。大多數(shù)現(xiàn)代的前端框架都有很多抽象,使我們能更輕松地為 Web 制作應用程序。但我們應該牢記一點,那就是使用這些東西可能會引發(fā)潛在的性能問題。
我經(jīng)常使用 Vue.js,所以決定探索一些我以前用過的指令,以前我用這些指令的時候完全沒考慮過它們可能對應用程序帶來的性能影響。其中有三條非常流行的指令進入了我的視線。
1. v-if 和 v-show
這兩條指令都是用來有條件地渲染元素的,但是它們背后的工作機制卻大不相同,因此用法也大相徑庭。v-if 一開始不會渲染組件,而只在條件為真時才渲染組件。這意味著當你多次切換組件的可見性時,就會不斷重新渲染。如果你要多次更改組件的可見性,那就不要使用這個功能。這會影響你的性能。
v-show 是一個很好的替代品。不管你是否啟用 CSS 都會渲染你的組件,但是只會根據(jù)條件是 true 還是 false 來決定組件是否可見。這種方法確實有其缺點,因為它不會將非必要組件的渲染推遲到你需要它們在屏幕上實際出現(xiàn)的時候。如果你的初始渲染沒那么復雜,那么它就很合適。
2. v-for
這條指令通常用來從數(shù)組中渲染列表。它有一個特殊的語法,形式為 item in list,其中 list 是源數(shù)據(jù)數(shù)組,而 item 是要迭代的數(shù)組元素的別名。默認情況下,Vue 在源數(shù)據(jù)數(shù)組上添加 watchers,每當發(fā)生更改時它就會觸發(fā)重新渲染。這種持續(xù)的重新渲染可能會對應用程序性能產(chǎn)生不利影響。如果你只想可視化對象,那么 Object.freeze() 是一個很好的解決方案,可以大大提高性能。但是請務必記住,你將無法更新組件或編輯對象數(shù)據(jù)。
在這個研究過程中我還意識到,Lighthouse 可能檢查的是以更直接的方式影響用戶體驗的應用性能指標,所以接下來我的疑問就是如何跟蹤服務器上的應用程序性能。
我們是不是太依賴直覺,是不是在假設開發(fā)人員知道自己在做什么,假設他們遵循的是最佳實踐?不管怎樣,這次經(jīng)歷讓我對單頁應用程序的性能產(chǎn)生了不同的看法。大家可以在 GitHub 上查看上述項目的存儲庫:
https://github.com/Maria218/GlobalGameJamThing