Categories
程式開發

我發現了Vue.js中的性能陷阱


我們做了一款單頁應用形式的遊戲,到了請求域的時候內存佔用爆表了。雖然遊戲大賽已經結束了,但是我依舊不能釋懷。這個問題一直困擾著我。問題出在Vue.js嗎?是Netlify嗎?還是因為我們的代碼有缺陷?我必須找出答案。

本文經原作者授權,由InfoQ中文站翻譯並分享。

我內心深處對遊戲的熱愛,讓我一直渴望能自己製作一些電子遊戲。幾個月前我開始將這種夢想變為現實,並第一次參加了全球遊戲大賽(Global Game Jam)。我和我的團隊使用Vue.js構建了一個名為“ZeroDaysLeft”的遊戲,其形式是Web端的單頁面應用程序。這款遊戲的主題是環境保護,我們考慮到商業活動對地球環境的影響,希望就這個話題做一些有益的探討。使用Vue.js製作的遊戲並不多。我的團隊遲到了一天,然後用猜拳的方式選擇了我們要用的框架;我們飛快地寫完了代碼,並在周末結束時做出了遊戲的可運行版本。在本地測試時一切都很順利。自然,我們為自己第一次寫出來的遊戲作品感到自豪,並希望與世界分享它。

我發現了Vue.js中的性能陷阱 1

可是問題出現了——當我們構建好應用並開始查詢域時,內存佔用爆表了。它幾乎沒法正常運行,不管換什麼機器都會卡住不動,即使在強大的基於Intel i7處理器的系統上程序也會崩潰。遊戲大賽的時間限制把我們拉回了現實,我們決定擱置生產性能問題,這樣起碼我們能做出一款能在自己的設備上運行的完整遊戲。就像大部分的“已完成”項目一樣,第二天我們就把它拋在腦後了。

但我自己沒法釋懷。它一直困擾著我。問題是出在Vue.js上嗎?是Netlify嗎?還是因為我們的取巧代碼?我必須找出答案。

調查性能下降的原因

我首先使用Lighthouse進行了快速測試。所幸Firefox為此提供了一個瀏覽器插件。下面就是我得到的結果。

我發現了Vue.js中的性能陷阱 2

89%的數字挺不錯的。實際上,與許多流行的網站相比,這個表現相當出色。這個測試指出了一些潛在問題,例如速度指數和第一次有意義且有內容的繪製步驟等。從理論上講,解決這些問題會進一步提高分數,但不一定能解決應用面臨的嚴重性能問題。

我們的遊戲中有一些圖像和音頻素材資源,但是兩者都不至於讓遊戲卡死在那裡。我們也可以對這些已經優化過的資源再過度優化一遍,但這可能根本就無濟於事。

這個測試無法讓我們真正找出可能導致這一性能問題的原因。於是我開始想:“該不會是Vue的問題吧?”這種想法會冒出來也沒什麼理由,但要是不檢查一下就是蠢了。我檢查了已部署站點的控制台,結果空白一片。但警告往往不會在生產中顯示。當我在本地進行相同操作時,一堆Vue警告讓我吃了一驚。

我發現了Vue.js中的性能陷阱 3

像大多數開發人員一樣,我對控制台警告沒那麼在意,覺得它們只是警告,而不代表錯誤;所以我一般會把注意力集中在其他地方。或許消除這些警告可以解決我的生產問題,我決定深入研究每個問題並修復它們。

所有這些警告均來自我創建的、用來顯示名為Cards.vue選項的組件,因此這個組件可能需要大量重寫。

我決定按順序解決這些控制台警告。

> [Vue warn]: Avoid using non-primitive value as key, use string/number value instead.
 found in 
 --->  at src/components/Cards.vue

Vue.js有很多指令,讓我們能更直觀地使用框架,比如說v-for就可以快速將數組渲染為列表。使用它時,我們需要一個 :key才能有效地重渲染組件。但我們將一個對像用作了一個鍵,這是非原始值,因此導致了這個錯誤。我決定將index.description用作一個新鍵,因為它是一個字符串,並且在值發生更改時可以更好地重新渲染。

> [Vue warn]: Duplicate keys detected: '[object Object]'. This may cause an update error.
found in
--->  at src/components/Cards.vue

將 :key更改為一個字符串(index.description)來解決上一個錯誤,就能解決這個重複鍵的錯誤。我們只能將字符串類型寫入DOM,因此當我們傳遞一個要渲染的對象時,該對象將轉換為等效的字符串(即[object Object]);並且因為這以前是我們的鍵,所以每個對像都將轉換為[object Object](除非對像有不同的值),進而會出現重複鍵警告。現在既然鍵不是對象,警告就會消失,效率也會提升。

> [Vue warn]: You may have an infinite update loop in a component render function.
found in
--->  at src/components/Cards.vue

就一個非常模糊的警告來說,這個警告似乎是最重要的:無限循環意味著內存消耗。這條消息並沒有告訴我們可能出了什麼問題,但它確實暗示了問題與組件中的render函數有關。也許是因為我們寫的代碼比較取巧,因此觸發了不間斷的更新,並佔用了大量的計算能力,以至於使瀏覽器和設備崩潰。

這條警告至少告訴我們要檢查Cards.vue,所以我的第一個想法是檢查組件中的反應屬性,因為這可能會導致錯誤。反應屬性在更改後會觸發重新渲染。

我們正在顯示index.days和index.description中的數據。但我們不會更改這些數據,我們從cardInfo數組獲得index。

> v-for="index in cardInfo.sort(() => Math.random() - 0.7).slice(0,4)"

我們使用這段代碼對數組中的元素進行隨機排序,然後將前四個元素顯示為玩家選擇的選項。當用戶單擊一個選項時將調用effects()函數,它除了會計算一個動作如何影響遊戲狀態外,還使用cardInfo上的拼接原型刪除前四個元素。

在Vue這種使用虛擬DOM的框架裡,用上諸如cardInfo之類的反應屬性後,每當數據屬性的值更改時都會觸發重新渲染。在我們的應用裡,我們會直接使用sort()原型來更改它,然後刪除元素來重新排序。所有這些都會觸發“無限”的重新渲染,從而引發警告。

我決定更改數據過濾的邏輯,並停止對反應屬性cardInfo的多次更改。我安裝了lodash.shuffle並定義了一個計算屬性shuffledList(),它將創建一個名為list的cardInfo副本。我對其應用了隨機排序操作,並返回了一個“frozen”結果,然後拆分開來顯示四張卡片。我們使用了Object.freeze(),它將使我們返回的對像不可變,從而完全停止了所有重新渲染操作。

至此,問題解決了。

掉進框架的坑

老實說,當我剛開始調查性能下降原因的時候,還覺得我肯定要優化很多資源才能解決問題。最後這個結果說明,在使用許多框架抽象時我們都必須非常小心——特別是在Vue中更是如此,只有在必要時才使用某條指令,而且用法一定不能出錯,因為它們絕對有自己的代價。

這還讓我開始思考自己做過的其他工作,其中應用程序可能會因為框架而出現不必要的性能問題。大多數現代的前端框架都有很多抽象,使我們能更輕鬆地為Web製作應用程序。但我們應該牢記一點,那就是使用這些東西可能會引發潛在的性能問題。

我經常使用Vue.js,所以決定探索一些我以前用過的指令,以前我用這些指令的時候完全沒考慮過它們可能對應用程序帶來的性能影響。其中有三條非常流行的指令進入了我的視線。

v-if和v-show

這兩條指令都是用來有條件地渲染元素的,但是它們背後的工作機制卻大不相同,因此用法也大相徑庭。 v-if一開始不會渲染組件,而只在條件為真時才渲染組件。這意味著當你多次切換組件的可見性時,就會不斷重新渲染。如果你要多次更改組件的可見性,那就不要使用這個功能。這會影響你的性能。

v-show是一個很好的替代品。不管你是否啟用CSS都會渲染你的組件,但是只會根據條件是true還是false來決定組件是否可見。這種方法確實有其缺點,因為它不會將非必要組件的渲染推遲到你需要它們在屏幕上實際出現的時候。如果你的初始渲染沒那麼複雜,那麼它就很合適。

v-for

這條指令通常用來從數組中渲染列表。它有一個特殊的語法,形式為item in list,其中list是源數據數組,而item是要迭代的數組元素的別名。默認情況下,Vue在源數據數組上添加watchers,每當發生更改時它就會觸發重新渲染。這種持續的重新渲染可能會對應用程序性能產生不利影響。如果你只想可視化對象,那麼Object.freeze()是一個很好的解決方案,可以大大提高性能。但是請務必記住,你將無法更新組件或編輯對像數據。

在這個研究過程中我還意識到,Lighthouse可能檢查的是以更直接的方式影響用戶體驗的應用性能指標,所以接下來我的疑問就是如何跟踪服務器上的應用程序性能。

我們是不是太依賴直覺,是不是在假設開發人員知道自己在做什麼,假設他們遵循的是最佳實踐?不管怎樣,這次經歷讓我對單頁應用程序的性能產生了不同的看法。大家可以在GitHub上查看上述項目的存儲庫,也歡迎大家在Twitter上和我打招呼。

原文鏈接:

https://stackoverflow.blog/2020/03/25/tracking-down-performance-pitfalls-in-vue-js/