一、背景
目前公司的電子合同
采用表單設計器
+合同業務
配合實現,做了半年多后終於上線,但是下邊員工普遍反映卡頓,甚至卡死,爆棧。尤其是新增和修改合同頁面,因為這部分數據量大,邏輯復雜,很容易崩潰,所以決定進行性能優化。
二、業務場景介紹
先來了解一下我們是怎么實現:
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倍。
整體效果如下:
今日成果,雖數月,但眾人拾柴,得以燎原,此非一人之功,謝而不及。