Skip to content

detached-dom-elements

說好的虛擬呢?記憶體爆炸啦!Σ(ˊДˋ;)

某個專案回報網頁會越來越卡,甚至崩潰。

查了一下是因為記憶體用量像鱈魚的體重一樣越來越高,都不知道是鯨魚還是鱈魚了。

鱈魚:「旁白不要給我亂念劇本 ლ(´ 口`ლ)


嘗試重現了一下,發現在虛擬滾動列表中,反覆滾動會讓記憶體不斷上升,而且還沒有辦法釋放。

讓我們來看看是怎麼回事。

虛擬滾動

若頁面一次載入太多元素,可能會導致頁面卡頓,甚至崩潰。

這時候我們可以透過虛擬滾動來解決這個問題。

虛擬滾動只會顯示可見區域的元素,同時也會預先載入(Overscan)可見區域外的一些元素,避免快速捲動時出現大量空白。

下方是一個虛擬滾動的互動說明,可以清楚看到可視區域、渲染區域和未渲染區域。

虛擬列表
#0
可視
#1
可視
#2
可視
#3
可視
#4
可視
#5
可視
#6
可視
#7
可視
#8
可視
#9
可視
#10
可視
#11
可視
#12
可視
#13
可視
#14
可視
#15
可視
#16
可視
#17
可視
#18
可視
#19
可視
#20
可視
#21
可視
#22
可視
#23
可視
#24
可視
#25
可視
#26
可視
#27
可視
#28
可視
#29
可視
#30
可視
#31
可視
#32
可視
#33
可視
#34
可視
#35
可視
#36
可視
#37
可視
#38
可視
#39
可視
#40
可視
#41
可視
#42
可視
#43
可視
#44
可視
#45
可視
#46
可視
#47
可視
#48
可視
#49
可視
#50
可視
#51
可視
#52
可視
#53
可視
#54
可視
#55
可視
#56
可視
#57
可視
#58
可視
#59
可視
#60
可視
#61
可視
#62
可視
#63
可視
#64
可視
#65
可視
#66
可視
#67
可視
#68
可視
#69
可視
#70
可視
#71
可視
#72
可視
#73
可視
#74
可視
#75
可視
#76
可視
#77
可視
#78
可視
#79
可視
#80
可視
#81
可視
#82
可視
#83
可視
#84
可視
#85
可視
#86
可視
#87
可視
#88
可視
#89
可視
#90
可視
#91
可視
#92
可視
#93
可視
#94
可視
#95
可視
#96
可視
#97
可視
#98
可視
#99
可視
整體樣貌
#0
可視區
#1
可視區
#2
可視區
#3
可視區
#4
可視區
#5
可視區
#6
可視區
#7
可視區
#8
可視區
#9
可視區
#10
可視區
#11
可視區
#12
可視區
#13
可視區
#14
可視區
#15
可視區
#16
可視區
#17
可視區
#18
可視區
#19
可視區
#20
可視區
#21
可視區
#22
可視區
#23
可視區
#24
可視區
#25
可視區
#26
可視區
#27
可視區
#28
可視區
#29
可視區
#30
可視區
#31
可視區
#32
可視區
#33
可視區
#34
可視區
#35
可視區
#36
可視區
#37
可視區
#38
可視區
#39
可視區
#40
可視區
#41
可視區
#42
可視區
#43
可視區
#44
可視區
#45
可視區
#46
可視區
#47
可視區
#48
可視區
#49
可視區
#50
可視區
#51
可視區
#52
可視區
#53
可視區
#54
可視區
#55
可視區
#56
可視區
#57
可視區
#58
可視區
#59
可視區
#60
可視區
#61
可視區
#62
可視區
#63
可視區
#64
可視區
#65
可視區
#66
可視區
#67
可視區
#68
可視區
#69
可視區
#70
可視區
#71
可視區
#72
可視區
#73
可視區
#74
可視區
#75
可視區
#76
可視區
#77
可視區
#78
可視區
#79
可視區
#80
可視區
#81
可視區
#82
可視區
#83
可視區
#84
可視區
#85
可視區
#86
可視區
#87
可視區
#88
可視區
#89
可視區
#90
可視區
#91
可視區
#92
可視區
#93
可視區
#94
可視區
#95
可視區
#96
可視區
#97
可視區
#98
可視區
#99
可視區
渲染區
可視區

UI 套件可能會有內建虛擬滾動元件,如果沒有、需求簡單,則可以使用 VueUse 的 useVirtualList

使用上非常簡單:

vue
<template>
  <div
    v-bind="containerProps"
    class="overflow-auto p-2 border border-gray-400/80 rounded opacity-80"
  >
    <div v-bind="wrapperProps">
      <div
        v-for="item in list"
        :key="item.index"
        class="mb-2 flex items-center justify-center h-[40px] bg-gray-200/50"
      >
        Row {{ item.index }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useVirtualList } from '@vueuse/core'

const allItems = Array
  .from({ length: 500 })
  .map((_, index) => ({ index, value: `${index}` }))

const { list, containerProps, wrapperProps } = useVirtualList(
  allItems,
  { itemHeight: 40 },
)
</script>

結果如下:

問題重現

不難注意到虛擬列表會不斷建立、銷毀 DOM 元素,如果我們反覆滾動,記憶體就會不斷上升。

讓我們快速滾動列表並打開 DevTools 使用 Performance Monitor(效能監視器)查看記憶體與 DOM 的變化。

dom-is-released-normally

可以注意到即使 DOM 元素一度衝到接近 40000 個,也會在特定時間點釋放並不會無限上升。

所以到底是怎麼回事呢?(́⊙◞౪◟⊙‵)


反覆交叉嘗試後,發現問題出在 input 元素,沒錯,就是那個樸實無華的輸入框。(◉◞౪◟◉ )

只要在剛剛的虛擬列表中加入 input 元素,記憶體就會不斷上升,不會自動釋放,直到記憶體爆炸。

vue
<template>
  <div
    v-bind="containerProps"
    class="overflow-auto p-2 border border-gray-400/80 rounded opacity-80"
  >
    <div v-bind="wrapperProps">
      <div
        v-for="item in list"
        :key="item.index"
        class="mb-2 flex items-center justify-center h-[40px] bg-gray-200/50"
      >
        Row {{ item.index }}

        <input
          v-model="value"
          class=" outline ml-4"
        >
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useVirtualList } from '@vueuse/core'
import { ref } from 'vue'

const allItems = Array
  .from({ length: 99999 })
  .map((_, index) => ({ value: `${index}` }))

const { list, containerProps, wrapperProps } = useVirtualList(
  allItems,
  { itemHeight: 40 },
)

const value = ref('codfish')
</script>

一樣不斷滾動列表,並查看 Performance Monitor。

dom-is-not-released

可以看到 DOM 數量不斷上升,完全沒有釋放的跡象。(;´༎ຶД༎ຶ`)


鱈魚:「真相出爐了,兇手就是 inputヽ(●`∀´●)ノ

同事:「可是一樣的範例,我的電腦沒有這個問題欸(´・ω・`)

鱈魚:...ヽ(́⊙◞౪◟⊙‵)ノ


使用不同瀏覽器交叉測試後,發現某些瀏覽器外掛會導致問題(例如:密碼管理工具),關閉外掛後就沒問題了,所以該怎麼辦呢?


鱈魚:「這個簡單,關閉外掛或只能用乾淨瀏覽器,不要拉倒 (・∀・)9

同事:「PM 在你後面,他非常火 ╭(°A ,°`)╮


我們沒辦法控制客戶的瀏覽器環境,只能調整 input 出現的時機了。

可以改成開啟編輯模式時才顯示 input 或使用像 Popup Edit 這種點擊後顯示浮動 input 的元件。

最後改成 Popup Edit 方案,不但問題解決惹,性能也提升了一點。ԅ( ˘ω˘ԅ)

總結 🐟

  • 虛擬列表可以確保大量資料顯示時,不會造成頁面卡頓。
  • 某些瀏覽器外掛會導致問題,調整 input 出現的時機,避免大量建立、銷毀 input 元素。