vue大型專案高效能優化----想說愛你真的不容易

俗的太不一樣發表於2020-09-19

一、背景

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

二、業務場景介紹

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

  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倍。

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

相關文章