前端性能優(yōu)化應(yīng)該怎么做?
前言
最近零零散散的對(duì)剛接手的一個(gè)新項(xiàng)目做了一些優(yōu)化,白屏、打包相關(guān)的內(nèi)容都涉及到了,寫一篇文章來記錄一下。
白屏相關(guān)
DNS預(yù)解析、資源預(yù)加載
對(duì)于項(xiàng)目中有很多靜態(tài)資源涉及到的公共域名,如g.alicdn.cmon,采用DNS預(yù)連接 + 解析:
<link rel="preconnect" crossorigin />
<link rel="dns-prefetch" />
對(duì)于項(xiàng)目中一些必要的JS資源,采用資源預(yù)加載,可以大幅度縮短資源加載時(shí)間:
<link rel="preload" as="script" />
<link rel="preload" as="script" />
結(jié)果:整體白屏?xí)r間降低400~600ms。
頁面級(jí)路由懶加載
原本項(xiàng)目打包出來的JS文件只有一個(gè)bundle.js,涵蓋了整個(gè)項(xiàng)目的業(yè)務(wù)代碼,對(duì)于業(yè)務(wù)方來說來說,可能訪問最多的就是新增和詳情兩個(gè)頁面,所以對(duì)于首屏加載是不友好的,應(yīng)該優(yōu)化成訪問哪個(gè)頁面加載對(duì)應(yīng)頁面的資源,基于Ice2.0調(diào)研,將路由中的組件都轉(zhuǎn)換為懶加載模式:
routes.ts
import { lazy, IRouterConfig } from 'ice';
// ice不支持layout組件設(shè)置為懶加載
import Layout from '@/layouts/BasicLayout';
const Home = lazy(() => import(/* webpackChunkName: 'Home' */ '@/pages/Home'));
const NotFound = lazy(() => import(/* webpackChunkName: 'NotFound' */ '@/components/NotFound'));
const ManualDetect = lazy(() => import(/* webpackChunkName: 'ManualDetect' */ '@/pages/ManualDetect'));
const AddMission = lazy(() => import(/* webpackChunkName: 'addMission' */ '@/pages/ReconnaissanceMission/add-mission'));
const MissionDetail = lazy(
() => import(/* webpackChunkName: 'missionDetail' */ '@/pages/ReconnaissanceMission/missionDetail'),
);
const NewMissionDetail = lazy(
() => import(/* webpackChunkName: 'newMissionDetail' */ '@/pages/ReconnaissanceMission/newMissionDetail'),
);
const NoPermission = lazy(() => import(/* webpackChunkName: 'NoPermission' */ '@/pages/NoPermission'));
const Board = lazy(() => import(/* webpackChunkName: 'Board' */ '@/pages/Board'));
const BusinessInsight = lazy(() => import(/* webpackChunkName: 'BusinessInsight' */ '@/pages/BusinessInsight'));
const ChuangDaoInsight = lazy(() => import(/* webpackChunkName: 'ChuangDaoInsight' */ '@/pages/ChuangDaoInsight'));
const Report = lazy(() => import(/* webpackChunkName: 'Report' */ '@/pages/Report'));
const routes: IRouterConfig[] = [
{
path: '/',
component: Layout,
children: [
{
path: '/manualDetect',
component: ManualDetect,
},
{
path: '/addMission',
component: AddMission,
},
{
path: '/MissionDetail',
component: MissionDetail,
},
{
path: '/newMissionDetail',
component: NewMissionDetail,
},
{
path: '/',
exact: true,
component: Home,
},
{
path: '/noPermission',
exact: true,
component: NoPermission,
},
{
path: '/board',
exact: true,
component: Board,
},
{
path: '/businessInsight',
exact: true,
component: BusinessInsight,
},
{
path: '/chuangDaoInsight',
exact: true,
component: ChuangDaoInsight,
},
{
path: '/report',
exact: true,
component: Report,
},
{
component: NotFound,
},
],
},
];
export default routes;
看一下效果。
在改動(dòng)前是這樣的:
圖片
無論訪問哪個(gè)頁面,請(qǐng)求固定的JS文件,大小為2.3MB。
改動(dòng)以后發(fā)版:
首屏刷新:
圖片
切換一個(gè)路由:
圖片
效果很明顯了,文件資源也小了很多,白屏?xí)r間自然就下降了。
詳細(xì)的文章在這里:
React中的懶加載以及在Ice中實(shí)踐
結(jié)果:白屏?xí)r間整體降低,請(qǐng)求資源大小整體下降。
構(gòu)建相關(guān)
優(yōu)化本地?zé)岣聲r(shí)間
項(xiàng)目本地?zé)岣聲r(shí)間比較慢,大約在8~9秒,基于ice運(yùn)行時(shí)中間件在每次代碼變更時(shí)加入緩存同時(shí)移除對(duì)node_module目錄下的babel轉(zhuǎn)換,可以寫一段這樣的代碼:
module.exports = ({ onGetWebpackConfig }) => {
onGetWebpackConfig((config) => {
config.module
.rule('tsx')
.test(/\.jsx?|\.tsx?$/)
.exclude.add(/node_modules/)
.end()
.use('babel-loader')
.tap((options) => {
return {
...options,
cacheDirectory: true,
};
});
});
};
在build.json中注入該插件:
{
// ...
"plugins": [
"@ali/build-plugin-faas",
[
"build-plugin-ignore-style",
{
"libraryName": "antd"
}
],
"@ali/build-plugin-ice-def",
"./src/index.ts"
]
}
圖片
結(jié)果:熱更新時(shí)間降低到4秒左右,降低50%。
構(gòu)建包大小優(yōu)化
CDN資源替代項(xiàng)目依賴包
利用Webpack模塊可視化工具,項(xiàng)目中的依賴是這樣的:
圖片
圖片
從上圖可以看到:在開發(fā)環(huán)境整個(gè)構(gòu)建包體積達(dá)到了19.44MB,echarts、antv、moment這些包,體積都比較大,達(dá)到了MB量級(jí),并且在項(xiàng)目中前兩者使用頻率很低,只有引用過一次,對(duì)于這種情況,考慮將依賴包轉(zhuǎn)換為CDN引入的方式,原因如下:
- 減少打包產(chǎn)物大??;
- 減少白屏?xí)r間;
- 版本固定,使用頻率低,通過CDN單獨(dú)引入還會(huì)有瀏覽器強(qiáng)緩存的效益;
通過Webpack中externals,取消對(duì)于node_modules中枚舉包的計(jì)算,并且在項(xiàng)目index.html中從CDN引入所列舉到的包。
{
// ...
"externals": {
"echarts": "echarts",
"moment": "moment"
},
}
externals這里的key、value值分別對(duì)應(yīng)npm中的包名和CDN引入后在window下的全局變量名,找包的CDN路徑很簡單,但是如何知道全局變量名是什么呢?
可以打開CDN鏈接,格式化代碼,大概是這個(gè)樣子的:
function(e, t) {
"object" == typeof exports && "object" == typeof module ? //判斷環(huán)境是否支持commonjs模塊規(guī)范
module.exports = t(require("vue")) :
"function" == typeof define && define.amd ? //判斷環(huán)境是否支持AMD模塊規(guī)范
define("ELEMENT", ["vue"], t) :
"object" == typeof exports ? //判斷環(huán)境是否支持CMD模塊規(guī)范
exports.ELEMENT = t(require("vue")) :
e.ELEMENT = t(e.Vue)
} ("undefined" != typeof self ? self: this,function(e){
//省略...
});
從這個(gè)JS文件可以看到,這個(gè)全局變量是ELEMENT咯~這塊更詳細(xì)的教程可以看一下這篇文章,這位博主總結(jié)的很全:
Webpack系列』—— externals用法詳解
代碼分割
這里利用Webpack現(xiàn)有的能力,對(duì)使用頻繁的第三方庫和模塊進(jìn)行統(tǒng)一抽離,這一部分可以寫在上面提到的Ice中間件里去:
module.exports = ({ onGetWebpackConfig }) => {
onGetWebpackConfig((config) => {
config.optimization.splitChunks({
cacheGroups: {
vendor: {
priority: 1,
test: /node_modules/,
chunks: 'initial',
minChunks: 1,
minSize: 0,
name: 'vendor',
filename: 'vendor.js',
},
common: {
chunks: 'initial',
name: 'common',
minSize: 100,
minChunks: 3,
filename: 'common.js',
},
},
});
});
};
抽離出來的模塊如圖:
圖片
結(jié)果:優(yōu)化后的構(gòu)建包體積為9.1MB,降低了50%以上大小。