慎用watch監聽——記一次Vue頁面卡頓排查

這是我參與更文挑戰的第9天,活動詳情查看: 更文挑戰css

開心,T恤到手~前端

策之不以其道,食之不能盡其材,鳴之而不能通其意,執策而臨之,曰:「天下無馬!」嗚呼!其真無馬邪?其真不知馬也!vue

運營人員反饋某商品列表頁面,每頁1000條時,頁面卡頓嚴重,已經影響使用,經實際測試,1000個條記錄時,頁面加載長達2分鐘+,而且會出現卡死狀況。遂開始優化之旅~~java

頁面元素

頁面主體部分截圖以下,每一個元素包括一張主圖,一些基本的信息字段。運營反饋當設置分頁數量爲1000時,會出現卡死,通過實際測試,能夠重現。數據庫

image.png

主體前端代碼分爲兩個部分,一個是包裹這些元素的父元素,一個是元素組件:後端

<!-- 父元素關鍵代碼 --> 
<div v-loading="productsLoading" class="choosingproduct-products">
     <!-- product-card爲子組件,使用v-model綁定列表元素 -->
      <product-card
        v-for="(item, index) in result.list"
        :key="index"
        v-model="result.list[index]"
        :showpoint="false"
      />
      <div style="width: 100%;margin-top: 15px;margin-bottom: 85px" class="center">
        <pagination
          :total="result.totalRow"
          :page.sync="query.pageNum"
          :limit.sync="query.numPerPage"
          @pagination="getSkus"
        />
  	</div>
</div>
複製代碼

子組件ProductCard的關鍵代碼以下:markdown

<template>
  <div class="productcard flex-col-start-start" @click="changeChoosed">
    <div class="flex-row-center-spacebetween" style="width: 100%">
       <!-- 關鍵點1:狀態切換使用了visibility -->
      <el-checkbox v-model="value.choosed" class="productcard-checkbox " :class="{'productcard-delete' : !value.choosed}" @change="changeChoosed" />
    </div>
    <div style="width: 100%" class="center">
       <!-- 關鍵點2: 圖片過多 -->
      <el-image :src="value.picurl" style="height: 114px;width: 114px" />
    </div>
    <div .... />
  </div>
</template>

<script>
export default {
  name: 'ProductCard',
  props: {
    value: {
      type: Object,
      default() {
        return {}
      }
    },
  },
  data() {
    return {
      product: {
        choosed: false
      }
    }
  },
  watch: {
     // 關鍵點3: 也是最重要的一點,watch監聽不當,致使性能與卡頓
     product: {
       deep: true,
       handler: function(newValue, oldValue) {
               // 向上層組件發送改變後的數據
        	this.$emit('input', this.product)
   	   }
    },
    value: {
      deep: true,
      handler: function() {
        this.product = this.value
      }
    }
  },
  created() {
    this.product = this.value
  },
  methods: {
    changeChoosed() {
      // 改變選中狀態
      this.product.choosed = !this.product.choosed
    }
  }
}
</script>

<style scoped lang="scss">
.productcard {
  .productcard-checkbox {
  	visibility: hidden;
  }

  .productcard-delete {
    visibility: hidden;
  }
}

 .productcard:hover {
   .productcard-checkbox {
   	 visibility: visible;
   }
   .productcard-delete {
    visibility: visible;
   }
 }
</style>

複製代碼

後端接口

後端根據前端選擇的條件,執行查詢SQL,返回數據,使用的數據庫鏈接池Druid+ActiveRecord模式。(對這一點感興趣的能夠檢索jfinal相關)curl

public Page<Record> getData() {
		......
		Page<Record> result = Db.paginate(pageNum, numPerPage, select, exceptSelect);
    	for(Record r : result.getList()) {
            display(r);
        }
	    return result;
}
複製代碼

分析步驟

由於後端的性能更好分析一些,本人也更熟悉後端,因此先從後端入手:svn

後端

通過測試,接口返回1000條數據的時間大約是17秒。post

  1. 這個時間至關長,有優化空間
  2. 前臺卡頓時間2分鐘不符合,不是前端卡死的問題關鍵

前端

使用Chrome-Performance監聽加載1000條數據的性能狀況,咱們獲得以下幾張圖:

image.png

image.png

從而肯定了卡頓問題主要在前端,並且從renderList咱們猜想,卡頓主要在於ProductCard這個組件的渲染上。

猜想

定位到了ProductCard組件後,咱們首先根據Network查看到圖片的加載時間較長,猜想多是圖片過多致使加載過長。

第二個,咱們注意到選中與不選中,咱們在CSS中是經過變換組件的visibility屬性來實現的。是不是visibility切換會致使組件渲染卡頓呢?咱們檢索資料發現visibility:hidden的性能實際比display:none要好。

第三個(終於找到你!),咱們在組件中使用了watch,同時監聽組件內部元素productprops中的value,當product變更時,會經過this.$emit('input', this.product)傳遞給外部的value,而value變更,又會觸發this.product = this.value,進而又致使product變更,造成近乎死循環。形成嚴重性能問題。

咱們獲得如下結論:

  1. 圖片過多致使加載過長
  2. 使用visibility
  3. 慎用watch

優化

後端

咱們使用parallelStream優化後端性能,優化後代碼以下。接口響應時間從17秒左右降低到5秒。

public Page<Record> getData() {
		......
		Page<Record> result = Db.paginate(pageNum, numPerPage, select, exceptSelect);
    	result.getList().parallelStream().forEach(r -> {
			display(r);
		});
	    return result;
}
複製代碼

前端

針對分析中的三點,咱們分別使用圖片懶加載和修復watch後,1000條數據頁面耗時大約爲10秒,且不在出現卡死現象。

優化後的watch以下:

.......
watch: {
    value: {
      deep: true,
      handler: function() {
        this.product = this.value
      }
    }
  },
created() {
   this.product = this.value
},
methods: {
    changeChoosed() {
      this.product.choosed = !this.product.choosed
	  // 向上層發送數據
      this.$emit('input', this.product)
    }
......
}
複製代碼

前端沒有什麼優化經驗,歡迎指正!

若不吝可點個贊!

相關文章
相關標籤/搜索