一、背景
目前公司的電子合同
採用表單設計器
+合同業務
配合實現,做了半年多後終於上線,但是下邊員工普遍反映卡頓,甚至卡死,爆棧。尤其是新增和修改合同頁面,因為這部分資料量大,邏輯複雜,很容易崩潰,所以決定進行效能優化。
二、業務場景介紹
先來了解一下我們是怎麼實現:
1. 因為我們公司合同變換頻繁,條款之間還有邏輯,所以做了個基礎服務
(說白了就是元件庫),為合同提供模板
2. 表單設計器作為基礎服務,打包成了元件庫,嵌入到合同專案,包括合同生成元件(拖拽生成合同模板)和合同預覽元件(載入資料庫中的合同模板資料)
3. 合同專案有一個模組管理頁面,可以對多個模板進行維護,比如可以選擇啟用哪個模板。
4. 合同的管理員負責維護模板,可以用表單設計器拖拽生成合同模板,提交後落入資料庫,每個合同型別可以同時啟用一個模板。
5. 最終下邊員工用的就是啟用的模板(尤其是這部門卡頓)
下面是電子合同的巨集觀泳道圖:
三、頁面介紹
- 合同模板管理頁
- 新增模板頁面
- 新建合同頁面
- 合同填寫頁面
好了,基本的業務邏輯和頁面就介紹這麼多,特別卡頓的頁面就是第四個頁面,下面我們分析一下卡頓的原因。
四、卡頓分析
1. 首先就是表單設計器的問題最嚴重,因為每一個元件需要很多配置項才能夠支撐元件的渲染,而一個合同是由上千個元件組成,經過測試,一個合同模板需要5MB的儲存空間(資料庫用的是MongoDB,儲存格式為字串,幾乎不影響),下面是一個輸入框的配置
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倍。
今日成果,雖數月,但眾人拾柴,得以燎原,此非一人之功,謝而不及。