vue大型項目高性能優化----想說愛你真的不容易


一、背景

  目前公司的電子合同采用表單設計器+合同業務配合實現,做了半年多后終於上線,但是下邊員工普遍反映卡頓,甚至卡死,爆棧。尤其是新增和修改合同頁面,因為這部分數據量大,邏輯復雜,很容易崩潰,所以決定進行性能優化。

二、業務場景介紹

  先來了解一下我們是怎么實現:

  1. 因為我們公司合同變換頻繁,條款之間還有邏輯,所以做了個基礎服務(說白了就是組件庫),為合同提供模板

  2. 表單設計器作為基礎服務,打包成了組件庫,嵌入到合同項目,包括合同生成組件(拖拽生成合同模板)和合同預覽組件(加載數據庫中的合同模板數據)

  3. 合同項目有一個模塊管理頁面,可以對多個模板進行維護,比如可以選擇啟用哪個模板。

  4. 合同的管理員負責維護模板,可以用表單設計器拖拽生成合同模板,提交后落入數據庫,每個合同類型可以同時啟用一個模板。

  5. 最終下邊員工用的就是啟用的模板(尤其是這部門卡頓)

下面是電子合同的宏觀泳道圖:
image

三、頁面介紹

  1. 合同模板管理頁
    image
  2. 新增模板頁面
    image
  3. 新建合同頁面
    image
  4. 合同填寫頁面
    image

  好了,基本的業務邏輯和頁面就介紹這么多,特別卡頓的頁面就是第四個頁面,下面我們分析一下卡頓的原因。

四、卡頓分析

  1. 首先就是表單設計器的問題最嚴重,因為每一個組件需要很多配置項才能夠支撐組件的渲染,而一個合同是由上千個組件組成,經過測試,一個合同模板需要5MB的存儲空間(數據庫用的是MongoDB,存儲格式為字符串,幾乎不影響),下面是一個輸入框的配置

image

  2. 表單設計器的實現用了大量的閉包管理業務,我們都知道,閉包是特別耗內存的。

  3. 合同模板巨復雜,由上萬個組件拼接而成,我把模板數據down下來看了一下,大約是16000多個組件,大小為3.4MB。

  4. 因為表單設計器中包括id,model,事件id都是前端隨機生成的,采用隨機字符串+時間戳的形式,一共46位。

  5. 合同項目屬於大型項目,業務場景及其復雜,包括合同管理,附件管理,合同列表,新增頁面,審批頁面等等,我計算了一下,光路由頁面就有三十多個,頁面,組件,樣式,業務巨多,如果不做處理,不卡才怪

五、性能優化

1. 第一次嘗試

  說一下我的優化思路:首先,電子合同由表單設計器和合同業務兩個項目共同完成,合同模板加載慢的原因是瀏覽器渲染了大量的模板數據,這些模板數據是由多個組組成的(大約12個),我第一想到的就是分組渲染,先加載一個組,先讓用戶看到頁面,然后在繼續加載,一個一個,最終加載完成。這也是被大家認可的方案。

  然后我就開始實現這個分組渲染,做了大概有二十多天吧,一點效果沒出來。

  先看一下渲染的代碼:

<template v-show="itemManage==='group'">
  <preview-item-template v-for="(item) in domainNodeList"
                        :key="item.id"
                        :formNode="item"
                        :parent="domainNodeList">
  </preview-item-template>
</template>

  上面就是所有組加載的代碼,這是一個v-for,做分組渲染,我想到使用vue的異步組件實現,但是這是一個循環,所有的組件注冊的都是同一個名字,這顯然是不能用異步組件的,除非注冊的是不同名字的組件,但是我想了很長時間都做出來效果,所以這二十多天,失敗了。

2.第二次嘗試

  上邊說了,模板加載慢是因為瀏覽器渲染了大量的數據,我們知道,js是單線程的,也就是說,所有任務只能在一個線程上完成,一次只能做一件事。前面的任務沒做完,后面的任務只能等着。因此js處理數據的能力有限,所以在朋友的建議下調研了一把webworker

  webworker的作用,就是為js創造多線程環境,允許主線程創建Worker線程,將一些任務分配給后者運行。在主線程運行的同時,Worker線程在后台運行,兩者互不干擾。

  看了一把文檔我第一時間覺得這個方案不可行。說到底我們就是想要webworker為我們開辟縣城用來處理大量數據,但是webworker處理的大數據,不是指數據量非常大,而是要從計算量來看,通常用時不能控制在毫秒級內的運算都可以考慮放在web worker中執行。而我們的合同模板數據恰恰是數據量大,並不需要做特別大的運算。

  第二次嘗試失敗。

3.第三次嘗試

  后來在同事的建議下決定采用ssr,也就是服務端預渲染。我們平常寫的vue項目打包后生成dist,運維會把這個文件夾放在服務器中,我們看到的頁面其實就是生成執行的render函數,這是比較耗時的。

  所謂的服務端渲染,就是在服務端生成靜態頁面,然后交給客戶端渲染。

  自己從零搭建一套服務端渲染的應用是相當復雜的,所以我最終選用了nuxt框架。關於nuxt框架我不多做介紹,可以自己去看文檔(傳送門)。這個框架有自己的腳手架,也是vue官方推薦的。

  經過了一周的時間,完成了從vue向nuxt的遷移,大部門頁面速度有了明顯的提升。

  除了我們想優化的新增合同頁面。

  經過分析,合同項目用到的組件庫有element-UI和我問自己的表單設計器,element只有部門組件支持ssr,像是表格和樹是不支持ssr的,所以就不存在服務端渲染了。

  我也曾嘗試過弄一把表單設計器,讓它支持ssr,但是並沒有效果,如果有誰知道,可以聯系我。

  很顯然,第三次也失敗了。

4.第四次嘗試

  命運總是很捉弄人,優化了一個多月的合同,速度並沒有顯著的提升,領導很着急,我也很着急。

  突然有一天,我在回家的途中,記得那天風雨交加,雷霆大作,一聲巨雷轟天響,把我好的idea都劈出來了。我一下子想到了分組加載的實現。

先來看一把代碼的實現(只展示了部分代碼):

<template>
  <div class="dialog-preview" v-show="!formLoading">
      <el-form  ref="previewForm" onsubmit="return false"
                :size="formSettingState.componentSize"
                @hook:mounted="formMounted"
                :model="formModels">

        <template v-show="itemManage==='group'">
          <preview-item-template v-for="(item) in cutDomainNodeList.one"
                                :key="item.id"
                                :formNode="item"
                                :parent="cutDomainNodeList.one">
          </preview-item-template>
        </template>
        <template v-if="itemManage==='group' && formLoadingTwo">
          <preview-item-template v-for="(item) in cutDomainNodeList.two"
                                :key="item.id"
                                :formNode="item"
                                :parent="cutDomainNodeList.two">
          </preview-item-template>
        </template>
         <template v-if="itemManage==='group' && formLoadingThree">
          <preview-item-template v-for="(item) in cutDomainNodeList.three"
                                :key="item.id"
                                :formNode="item"
                                :parent="cutDomainNodeList.three">
          </preview-item-template>
        </template>
        </template>
      </el-form>
  </div>
</template>
<script>
export default {
    data() {
        return {
          formLoading: true,
          formLoadingTwo: false,
          formLoadingThree: false
        }
    },
    computed: {
        cutDomainNodeList () {
          let { domainNodeList } = this;
          let length = domainNodeList.length;
          if ( length <= 4 ) {
            return {
              one: domainNodeList
            }
          }else {
            return {
              one: domainNodeList.filter((el, index) => index <=2 ),
              two: domainNodeList.filter((el, index) => index>2 && index <=5 ),
              three: domainNodeList.filter((el, index ) => index > 5)
            }
        }
    },
    methods: {
        formMounted () {
          setTimeout(() => { this.formLoading = false },  500);
          setTimeout(() => { this.formLoadingTwo = true },  700);
          setTimeout(() => { this.formLoadingThree = true},  900);
        }
    }
}

分塊加載實現思路:

   1. 首先我把模板數據這個list利用計算屬性先做了個判斷,如果數組長度小於4,證明數據量較小,不需要分塊加載,如果大於4證明數據量大,需要進行分塊加載

   2. 分塊加載是根據數組索引過濾的,第一塊是0-2組,第二塊是2-5組,第三塊是索引大於5的(也可以分割的跟細),然后再頁面中分別遍歷渲染

   3. 看一下html中的el-form這個標簽,里邊有個@hook:mounted="formMounted"這句話,@hook:+生命周期代表在這個生命周期時執行,我們等mounted執行完延時500mm開始加載第一塊,700mm加載第二塊,900毫秒加載第三塊,這樣分塊加載的效果就出來了。

六、其他方面優化

   首先添加了骨架屏組件,讓用戶在等待的時候能看到過渡效果。

   上面提到,合同模板大約在3.4MB,這個就是個純json,讓瀏覽器一下子加載這個么大的數據難免卡頓,所以我就在想能不能優化一下模板大小,從而能夠提升加載速度。

   表單設計器中包括id,model,事件id都是前端隨機生成的,采用隨機字符串+時間戳的形式,一共46位,一個英文字符就是一個字節,這就是46個字節,所以我們可以縮短一下隨機數的長度,從而減少一下模板大小。

   最終選用了26位隨機數,我算了一下,大約能減少一半大小。

   后來我們讓測試人員新生成了一個模板,果然,新模板大小1.44MB,縮短了一倍還多。

   其他方面,我們知道表單設計器有些配置做的不到位,所以管理員不得不換個別的方式拖拽模板,所以我們加了一些配置項,從而使管理員可以少拖拽一些組件。這部分優化下來,模板大小大約減少了300多kb.

   我們還可以優化一下表單設計器的代碼,把閉包換個實現方式,應該也能提高加載速度,后續會做這些。

   合同業務項目也優化了一些接口,代碼,前后端交互方式,以及頁面的交互方式提高了性能和視覺效果。

七、總結

   這是我第一次費這么大勁做vue項目的性能優化,雖然坎坷,但也留下了好結果,我們從最初加載需要50秒甚至一分鍾,到現在10秒左右就能加載成功,速度提高可近5倍。
整體效果如下:
image

   今日成果,雖數月,但眾人拾柴,得以燎原,此非一人之功,謝而不及。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM