1.问题简述
项目搭建完成后,首次加载速度缓慢,严重影响交互
2.分析优化
多余依赖:
项目采用jeecg2.0作为基础框架,原本的框架包含众多不被需要的依赖。
首先删除不需要的页面路由文件,逐个查看依赖的引用关系,删除相关无用页面与无引用依赖
moment.js仅保留中文
//只保留中文语言包,chainWebpack下
chainWebpack: (config) => {
config.plugin('ContextReplacementPlugin').use(webpack.ContextReplacementPlugin, [/moment[/\]locale$/, /zh-cn/])
}
@ant icons按需加载
Ant Design Vue 1.2.x起,icon偏大,推荐按需引入
chainWebpack: (config) => {
config.resolve.alias
.set('@ant-design/icons/lib/dist$', resolve('src/icons.js'))
}
// src/icons.js
export {
ReloadOutline,
CloseCircleFill,
ExclamationCircleFill,
CloseCircleOutline,
CalendarOutline,
SearchOutline,
PlusOutline,
ExportOutline,
ImportOutline,
DownloadOutline,
HddOutline,
FilterOutline,
SyncOutline,
InfoCircleFill,
DownOutline,
LockOutline,
UnlockOutline,
DeleteOutline,
QuestionCircleOutline,
CheckCircleOutline,
CloseOutline,
GoldOutline,
CaretDownFill,
CaretUpFill
} from '@ant-design/icons'
echarts按需加载
// utils/charts.js
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from 'echarts/core'
/** 引入柱状图and折线图图表,图表后缀都为 Chart */
import { BarChart, LineChart, PieChart } from 'echarts/charts'
// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LegendComponent,
DataZoomComponent,
GraphicComponent
} from 'echarts/components'
// 标签自动布局,全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features'
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers'
// 注册必须的组件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
BarChart,
LabelLayout,
UniversalTransition,
CanvasRenderer,
LineChart,
LegendComponent,
DataZoomComponent,
GraphicComponent,
PieChart
])
// 导出
export default echarts
// 引入
import echarts from '@utils/charts.js'
splitchunks拆包
拆包并不能直接减小打包体积,原理是将原本一个整体,拆成多个个体,借助浏览器可创建多个下载线程的能力实现并行加载。(在项目部分资源偏大明显,即拉低或均匀整体资源加载的峰值时间)
splitChunks: {
// 表示选择哪些 chunks 进行分割,可选值有:async,initial和all
chunks: "async",
// 表示新分离出的chunk必须大于等于minSize,注:新分离,原本小于30的可以小于30
minSize: 30 * 1024,
// 表示新分离出的chunk必须小于maxSize,注:同上,且存在单个文件就大于配置的,也不进行拆分
maxSize: 100 * 1024,
// 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
minChunks: 1,
// 表示按需加载文件时,并行请求的最大数目。默认为5。
maxAsyncRequests: 5,
// 表示加载入口文件时,并行请求的最大数目。默认为3。
maxInitialRequests: 3,
// 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js
automaticNameDelimiter: '~',
// 设置chunk的文件名。默认为true。当为true时,splitChunks基于chunk和cacheGroups的key自动命名。
name: true,
// cacheGroups 下可以可以配置多个组
// 每个组根据test设置条件,符合test条件的模块,就分配到该组。
// 模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。
// 默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
cacheGroups: {
vendors: {
test: /[\/]node_modules[\/]/,
priority: -10
},
// default:
{
minChunks: 2,
priority: -20,
reuseExistinGChunk: true}
}
}
// chainWebpack下
config.optimization.splitChunks({
chunks: 'all',
// 以下布置拆包内容
cacheGroups: {
jspdf: {
name: 'chunk-jspdf',
priority: 30,
test: /[\/]node_modules[\/]_?jspdf(.*)/
},
echarts: {
name: 'chunk-echarts',
priority: 30,
test: /[\/]node_modules[\/]_?echarts(.*)/
},
ant: {
name: 'chunk-ant',
priority: 50,
test: /[\/]node_modules[\/]_?@ant-design|ant-design-vue|@antv(.*)/
}
}
})
合并打包
大多数时候我们不需要追究极限的性能需求,但如果你希望
举例:
假设拆包结果:
Antd : 600k
Moment : 80k
app : 200k
此时我们最大的单元包就是按需加载时的antd,
其余包最大的也不超过antd体积的1/3,
这时问题出现,antd本身是否还可以进行拆包,进行二度均分
答案是可以,但是由于antd本身已经是单元依赖
我们需要通过限制最大包体积maxSize进行大小拆分(即打散),可以拆分成1-几百个极小单元
此时空间性能转化为了多网络请求的时间性能,我们需要再对散包合并,即合并打包
注意:
合并打包能力与拆包存在顺序关系
为避免出现不合预期的效果,推荐先拆再合
// 通过限制minChunkSize,和maxChunks 能打出很多情况,选取你自身期望的效果
config.plugin('chunkPlugin').use(webpack.optimize.LimitChunkCountPlugin, [{
maxChunks: 5,
minChunkSize: 10000
}])
Babel-plugin-import 按需ant-design
原理:在你按需引入ant组件时隐性加载对应样式,减小体积
// 手动按需
import Button from 'vant/lib/button';
import 'vant/lib/button/style';
// babel自动按需
import { Button } from 'vant';
Vue.use(Button);
// 等同于手动按需
// 安装
npm install babel-plugin-import
module.exports = {
presets: [['@vue/app', { useBuiltIns: 'entry' }]],
plugins: [['import', { libraryName: 'ant-design-vue', libraryDirectory: 'es', style: 'css' }]]
}
font-spider压缩字体文件(如果字体文件使用量少且内容固定)
引用字体文件偏大时,如果只有例如首页标题等固定少量字体使用,可以打包仅包含该字体的字体文件
例如:用了dnn字体,dnn字体包2M,只打包dnn则只有30k。
暂时无法在飞书文档外展示此内容
// 安装(演示包在上面,需要的话自行下载)
npm install font-spider -g
// 运行
font-spider *.html
组件全局注册组件
main.js中全局注册的组件,及其相关依赖固定会在首屏渲染时引用(不受路由按需加载等影响,固定初次加载时引用)。
对部分引用较少或不需要全局注册的组件,放到对应的页面中引用,保证其页面使用时再加载对应依赖
即如下方式:
不推荐全局注册组件(下面的例子是不推荐写法)
可以考虑根据引用情况,下放到实际引用的组件处导入
import JModal from './JModal';
import JFormContainer from './JFormContainer.vue';
import JTreeSelect from './JTreeSelect.vue';
import JImageUpload from './JImageUpload.vue';
import JDate from './JDate.vue';
import JEllipsis from './JEllipsis.vue';
import JInput from './JInput.vue';
import JEasyCron from '@/components/jeecg/JEasyCron';
// jeecgbiz
import JSelectDepart from '../jeecgbiz/JSelectDepart.vue';
import JSelectMultiUser from '../jeecgbiz/JSelectMultiUser.vue';
import JSelectPosition from '../jeecgbiz/JSelectPosition.vue';
import JSelectUserByDep from '../jeecgbiz/JSelectUserByDep.vue';
// 引入需要全局注册的js函数和变量
import { Modal, notification } from 'ant-design-vue';
export default {
install(Vue) {
Vue.use(JModal);
Vue.component('JDate', JDate);
Vue.component('JEllipsis', JEllipsis);
Vue.component('JFormContainer', JFormContainer);
Vue.component('JImageUpload', JImageUpload);
Vue.component('JInput', JInput);
Vue.component('JTreeSelect', JTreeSelect);
// jeecgbiz
Vue.component('JSelectDepart', JSelectDepart);
Vue.component('JSelectMultiUser', JSelectMultiUser);
Vue.component('JSelectPosition', JSelectPosition);
Vue.component('JSelectUserByDep', JSelectUserByDep);
Vue.component(JEasyCron.name, JEasyCron);
// 注册全局js函数和变量
Vue.prototype.$Jnotification = notification;
Vue.prototype.$Jmodal = Modal;
},
};
区分路由按需引入
该框架中采用require的方式进行路由页面引入
const componentPath = (resolve) => require([`@/${component}.vue`], resolve);
使用这种方式引入对应页面的组件vue时发现并不能根据路由引入依赖,而是初次加载时就加载全部依赖,需要更改为import引入,如果你的项目已经使用了import方式,请不要修改。
// 生成嵌套路由(子路由)
function generateChildRouters(data) {
const routers = [];
for (const item of data) {
let component = '';
if (item.component.indexOf('layouts') >= 0) {
component = `components/${item.component}`;
} else {
component = `views/${item.component}`;
}
// eslint-disable-next-line
let URL = (item.meta.url || '').replace(/{{([^}}]+)?}}/g, (s1, s2) => eval(s2)); // URL支持{{ window.xxx }}占位符变量
if (isURL(URL)) {
item.meta.url = URL;
}
// const componentPath = (resolve) => require([`@/${component}.vue`], resolve);
const menu = {
path: item.path,
name: item.name,
redirect: item.redirect,
hidden: item.hidden,
component: () => import(`@/${component}.vue`), // 这里!!!!!!!!!!!
meta: {
title: item.meta.title,
icon: item.meta.icon,
url: item.meta.url,
permissionList: item.meta.permissionList,
keepAlive: item.meta.keepAlive,
/* update_begin author:wuxianquan date:20190908 for:赋值 */
internalOrExternal: item.meta.internalOrExternal,
/* update_end author:wuxianquan date:20190908 for:赋值 */
componentName: item.meta.componentName,
},
};
if (item.alwaysShow) {
menu.alwaysShow = true;
menu.redirect = menu.path;
}
if (item.children && item.children.length > 0) {
menu.children = [...generateChildRouters(item.children)];
}
if (!item.route || item.route !== '0') {
routers.push(menu);
}
}
return routers;
}
nginx压缩
资源压缩能大量的减少时间,但本质属于运维配置项,需要沟通运维人员
# gzip config
gzip on;#开启压缩
gzip_min_length 1k;#低于1k的不压缩
gzip_comp_level 9;#压缩程度
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;#对哪些mine资源开启Gzip压缩功能
gzip_vary on;#是否在响应报文首部插入“Vary: Accept-Encoding”
gzip_disable "MSIE [1-6].";#针对不同种类客户端发起的请求,选择性地关闭Gzip功能,这里意思是禁用IE6 gzip功能
手动prefetch
webpack会自动创建prefetch链接,增加首屏渲染时间
如果需要进行prefetch,推荐通过prefetch标签手动管理
// prefetch是一种 resource hint,
// 用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。
// 移除 prefetch避免加载多余的资源
config.plugins.delete('prefetch')
3.vue.config.js样例
const webpack = require('webpack');
const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin');
function resolve(dir) {
return path.join(__dirname, dir);
}
// vue.config.js
module.exports = {
/*
Vue-cli3:
Crashed when using Webpack `import()` #2463
https://github.com/vuejs/vue-cli/issues/2463
*/
// 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
productionSourceMap: false,
// qiankuan打包时放开
// outputDir: "../dist/main",
// 多入口配置
// pages: {
// index: {
// entry: 'src/main.js',
// template: 'public/index.html',
// filename: 'index.html',
// }
// },
// 打包app时放开该配置
publicPath: './',
configureWebpack: (config) => {
// 生产环境取消 console.log
if (process.env.NODE_ENV === 'production') {
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true;
}
},
chainWebpack: (config) => {
config.plugins.delete('prefetch');
config.resolve.alias
.set('@$', resolve('src'))
.set('@api', resolve('src/api'))
.set('@assets', resolve('src/assets'))
.set('@comp', resolve('src/components'))
.set('@views', resolve('src/views'))
.set('@utils', resolve('src/utils'))
.set('@ant-design/icons/lib/dist$', resolve('src/icons.js'));
// 生产环境,开启js\css压缩
if (process.env.NODE_ENV === 'production') {
config.plugin('compressionPlugin').use(
new CompressionPlugin({
test: /.(js|css|less)$/, // 匹配文件名
threshold: 10240, // 对超过10k的数据压缩
deleteOriginalAssets: false, // 不删除源文件
}),
);
//只保留中文语言包
config
.plugin('ContextReplacementPlugin')
.use(webpack.ContextReplacementPlugin, [/moment[/\]locale$/, /zh-cn/]);
config.optimization.splitChunks({
chunks: 'all'
});
}
// 配置 webpack 识别 markdown 为普通的文件
config.module.rule('markdown').test(/.md$/).use().loader('file-loader').end();
},
css: {
loaderOptions: {
less: {
modifyVars: {
/* less 变量覆盖,用于自定义 ant design 主题 */
'primary-color': '#3D6EFF',
'link-color': '#3D6EFF',
'border-radius-base': '2px',
},
javascriptEnabled: true,
},
},
},
devServer: {
port: 3000,
// hot: true,
// disableHostCheck: true,
// overlay: {
// warnings: false,
// errors: true,
// },
// headers: {
// 'Access-Control-Allow-Origin': '*',
// },
proxy: {
/* '/api': {
target: 'https://mock.ihx.me/mock/5baf3052f7da7e07e04a5116/antd-pro', //mock API接口系统
ws: false,
changeOrigin: true,
pathRewrite: {
'/jeecg-boot': '' //默认所有请求都加了jeecg-boot前缀,需要去掉
}
}, */
/* 注意:jeecgboot前端做了改造,此处不需要配置跨域和后台接口(只需要改.env相关配置文件即可)
issues/3462 很多人此处做了配置,导致刷新前端404问题,请一定注意 */
'/jeecg-boot': {
target: 'http://localhost:8080',
ws: false,
changeOrigin: true,
},
},
},
lintOnSave: undefined,
};