記一次 Nuxt.js 登錄頁性能優(yōu)化(性能提升十倍加)
Nuxt.js 登錄頁性能優(yōu)化
"color:rgb(255,255,255);">前言
最近有測試和 local 投訴,我們管理系統(tǒng)的登錄頁面經(jīng)常加載很久,常常會(huì)有頁面已經(jīng)出來了,但是點(diǎn)擊登錄毫無反應(yīng),直到全部加載后才能登錄。于是,他們提出讓我們?nèi)?yōu)化。
這是一個(gè)好問題,登錄頁雖然不是移動(dòng)端那種首頁,但也是最先呈現(xiàn)給內(nèi)部用戶的。
"color:rgb(255,255,255);">定位耗時(shí)
遇到這種問題,首先需要找出耗時(shí)都花在了哪里,然后再去想具體辦法去解決。
首先,打開登錄頁面控制面板,Disable Cache 之后查看一下每個(gè)資源的耗時(shí)。

從圖上可以明顯看出來,有一個(gè) 2.2m 的文件足足耗時(shí)5s之久,文件的耗時(shí)主要在下載上面,看來主要的性能瓶頸就在這里了。
由于 JS 文件在騰訊云 CDN 上面配置了協(xié)商緩存(etag),所以在第二次加載的時(shí)候速度提升非常大,基本上不到 1s 就可以加載出來了。

那么這個(gè)大文件是什么文件呢?
我去 Jenkins 上看一下構(gòu)建記錄,在 build 的時(shí)候看到這個(gè)文件就是基于第三方包打出來的 vendors 文件。

"color:rgb(255,255,255);">webpack4 splitChunks
既然知道這個(gè)是 vendors 文件了,那就來分析一下 webpack 構(gòu)建。
在 webpack4 里面出現(xiàn)了 splitChunk 來拆分 chunk 文件,webpack4 會(huì)有一個(gè)默認(rèn)的 vendors chunk,它會(huì)把 node_modules 都給打成一個(gè)包,類似于:
optimization:?{
????"color:#d19a66;">splitChunks:?{
??????"color:#d19a66;">chunks:?"color:#98c379;">'initial',
??????"color:#d19a66;">cacheGroups:?{
????????"color:#d19a66;">vendors:?{****
??????????test:?"color:#98c379;">/[\\/]node_modules[\\/]/,
??????????"color:#d19a66;">priority:?"color:#d19a66;">-10
????????}
??????}
????}
??}
只不過,Nuxt 在這個(gè)基礎(chǔ)上又拆分出了一個(gè) commons ,配置規(guī)則如下:
optimization.splitChunks.cacheGroups.commons?=?{
??"color:#d19a66;">test:?"color:#98c379;">/node_modules[\\/](vue|vue-loader|vue-router|vuex|vue-meta|core-js|@babel\/runtime|axios|webpack|setimmediate|timers-browserify|process|regenerator-runtime|cookie|js-cookie|is-buffer|dotprop|nuxt\.js)[\\/]/,
??"color:#d19a66;">chunks:?"color:#98c379;">'all',
??"color:#d19a66;">priority:?"color:#d19a66;">10,
??"color:#d19a66;">name:?"color:#56b6c2;">true
}
priority 代表優(yōu)先級,如果兩個(gè) cacheGroups 里面都引用了同一個(gè)庫,那么就根據(jù)優(yōu)先級來判斷優(yōu)先把這個(gè)庫打進(jìn)哪個(gè) chunk 里面。
很明顯 commons 的優(yōu)先級要高于 vendors,所以會(huì)把 test 規(guī)則匹配到的第三方包優(yōu)先拆分出來,這幾個(gè)主要是 Nuxt 中依賴的一些庫。
本地執(zhí)行了一次 analyze 后,得到的構(gòu)建圖是這樣的,可以看出來 vendors 明顯遠(yuǎn)比其他的包都要大,尤其是 xlsx、iview、moment、lodash 這幾個(gè)庫,幾乎占了一大半體積。

"color:rgb(255,255,255);">優(yōu)化
生成多 HTML
既然知道 vendors 包里面都是一些第三方庫了,那么是否可以只打出登錄頁依賴的第三方庫,然后只去加載這個(gè) chunk 文件呢?
我看了一下登錄頁邏輯很簡單,不需要 lodash、moment,甚至連 iview 都不需要,完全可以自己去實(shí)現(xiàn)樣式,這樣就不必去加載體積這么大的 vendors chunk 了。
真是個(gè)好主意,可是問題來了,怎么才能不去加載 vendors 呢?
如果是在 webpack 里面,這個(gè)很容易,我們可以通過 html-webpack-plugin 來加載多個(gè) HTML 文件,針對登錄頁生成一個(gè) HTML 文件,讓它只去加載自身依賴的 chunk 文件。
于是我去看了一下 Nuxt 源碼,發(fā)現(xiàn)這里還是暴露了配置給我們?nèi)ザx一個(gè)新的 HTML 模板的。
當(dāng)然,到最后我也沒去嘗試這種方法,只是覺得應(yīng)該可以實(shí)現(xiàn)。

從 HTML 模板中刪除
Nuxt 會(huì)暴露給我們一個(gè) app.html 模板文件,它會(huì)在服務(wù)端渲染出來數(shù)據(jù),最后替換到這個(gè)文件里面。
<"color:#e06c75;">html?{{?HTML_ATTRS?}}>
??<"color:#e06c75;">head?{{?HEAD_ATTRS?}}>
????{{?HEAD?}}
??"color:#e06c75;">head>
??<"color:#e06c75;">body?{{?BODY_ATTRS?}}>
????{{?APP?}}
??"color:#e06c75;">body>
"color:#e06c75;">html>
那么我們有沒有可能在 Nuxt 替換這些占位符之前先去除掉不需要加載的 chunk 文件呢?其實(shí)也是可以的,只是需要修改到 Nuxt 的源碼。
修改了源碼之后,還需要用 patch-package 去打一個(gè)補(bǔ)丁,這樣就可以做到修改 node_modules 里面的代碼。
打開項(xiàng)目的 node_modules 文件夾,找到 @nuxt/vue-renderer/dist/vue-renderer.js,在 SSRRenderer 這個(gè)類里面的 render 方法中,我們可以看到如下代碼:
imagem.script.text({ body: true }) 這句代碼拿到的就是最后頁面上渲染出來的 script 標(biāo)簽,如果在這里匹配到 vendors 包,把它給排除掉,之后在頁面上就不會(huì)加載這個(gè) JS 文件了。
我這里的方案是這樣的,首先把登錄頁不需要且體積很大的幾個(gè)包(iview、moment、lodash)給單獨(dú)打了一個(gè) my-vendors 的包。
在 Nuxt 源碼中用正則表達(dá)式去匹配這個(gè)文件名,然后手動(dòng) replace 掉(記得要把 link 標(biāo)簽里面預(yù)加載的也一起替換掉)
"color:#5c6370;font-style:italic;">//?nuxt.config.js
config.optimization.splitChunks.cacheGroups.myVendors?=?{
??????????"color:#d19a66;">test:?"color:#98c379;">/node_modules[\\/](view-design|moment|moment-timezone|dayjs|crypto-js|simple-uploader\.js|vue2-google-maps|vuex-class|axios)[\\/]/,
??????????"color:#5c6370;font-style:italic;">//?cacheGroupKey?here?is?`commons`?as?the?key?of?the?cacheGroup
??????????automaticNamePrefix:?"color:#98c379;">'my-vendors',?"color:#5c6370;font-style:italic;">//?文件名以?my-vendors?為前綴
??????????name:?"color:#56b6c2;">true,
??????????"color:#d19a66;">chunks:?"color:#98c379;">'all',
??????????"color:#d19a66;">priority:?"color:#d19a66;">10
??????????reuseExistingChunk:?"color:#56b6c2;">true
}
"color:#5c6370;font-style:italic;">//?vue-renderer.js
"color:#c678dd;">const?scripts?=?APP.match("color:#98c379;">/(\
??????
??????
????????
????????
??????????
????????????
??????????????
??????????????????????????????????slot="title"
??????????????????style="text-transform:?capitalize;?color:?#595d65;?font-size:?16px;?display:?flex;?height:?25px;"
????????????????>
??????????????????${
????????????????????config.cdnServer.staticUrl
??????????????????}/static/admin-website/logo.png"?alt="logo"?class="login-logo"?/>
????????????????
??????????????
??????????????
????????????????
??????????????????
??????????????????Sign?in?with?Google
????????????????
??????????????
????????????
??????????
????????
????????
????????
??????
????