问题现象:
系统登录成功后,在加载出首页和菜单前会有很长一段时间的白屏
下面的图片是在本地搭建环境直接访问的网络请求包,从图中可以看出一共有145个请求,加载了36.87分钟,这是在本地环境访问的情况下,如果在服务器上考虑到网络等因素会达到1分钟左右
在界面没有任何提示的情况下,且处于白屏状态,是个很糟糕的用户体验,当然可以考虑在加载时间内加上加载进度条,但这只能说是改善了用户体验,并没有解决根本的问题
下面我第一想到的是对网络请求进行分析,试图从网络请求中找到耗时比较长的请求,下图是根据网络请求耗时排序后的情况,在其中并没有找到非常耗时的请求,最大的请求耗时也才1.64秒(这里是菜单加载的接口,在我的另一篇文章中有对他的优化思路),即使去优化他也并不能解决问题
既然不能简单的找到问题,那么我们只能通过性能分析工具来尝试找到原因了,打开chrome的性能页签,点击开始录制,然后重新刷新页面
等界面加载完成后点击停止
下面是分析结果的情况,在概要中我们可以看到执行脚本占用了比较多的时间,整体耗时46.39秒
到了这里问题其实可以确定为js脚本执行慢导致的问题了
下面就具体分析下是什么地方的代码导致的慢
在由上至下页签可以看到耗时比较长的脚本,但是此处代码被混淆了,不容易判断到底是哪里的代码(如果对代码非常熟悉,直接看混淆后的代码也可以分析,我采用的是这种方法),为了方便查看代码情况,我们启动前端开发服务,来进行访问
下面是换成本地开发模式后的性能分析结果,总时长增加到了1.9分钟,不过看概要是空闲时间增加了,应该跟vite的开发模式和发布模式不一样有关,但是这里可以看到脚本执行了47秒钟的问题依然存在,所以我们的重点还是放在分析脚本耗时过多的原因
通过下面的分析结果已经很明确的定位到代码的位置了,是一个递归解析动态路由的代码
点击右侧对应的代码可以跳转到详细的代码信息,左侧还有执行耗时
通过代码分析的结果看有两处耗时比较长的地方
第一处
我们分析具体的代码,这里是一个递归调用,假如routes比较多的时候,调用次数会非常多
let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
// Dynamic introduction
export function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../views/**/*.{vue,tsx}');
let subModules = import.meta.glob('../../../apps/**/src/index.{js,ts}', {
eager: true,
import: 'dynamicViewsModules',
});
for (const key in subModules) {
if (Object.prototype.hasOwnProperty.call(subModules, key)) {
const element = subModules[key];
dynamicViewsModules = Object.assign({}, dynamicViewsModules, element);
}
}
if (!routes) return;
routes.forEach((item) => {
if (!item.component && item.meta?.frameSrc) {
item.component = 'IFRAME';
}
const { component, name } = item;
const { children } = item;
if (component) {
const layoutFound = LayoutMap.get(component.toUpperCase());
if (layoutFound) {
item.component = layoutFound;
} else {
item.component = dynamicImport(dynamicViewsModules, component as string);
}
} else if (name) {
item.component = getParentLayout();
// 当前为末级菜单时,会造成其他菜单无法打开的bug,所以加下面代码处理
if (!item.children) {
item.component = EXCEPTION_COMPONENT;
}
}
children && asyncImportRoute(children);
});
}
在后台查询了下数据库中菜单的数量为6340,虽然有点多,但是还没到不能忍受的程序,我们继续分析上面的代码
在上面的调试中,可以看到dynamicViewsModules的数量是非常多的,有6151个,
第二处
function dynamicImport(
dynamicViewsModules: Record<string, () => Promise<Recordable>>,
component: string,
) {
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
const k = key.replace('../../views', '');
const startFlag = component.startsWith('/');
const endFlag = component.endsWith('.vue') || component.endsWith('.tsx');
const startIndex = startFlag ? 0 : 1;
const lastIndex = endFlag ? k.length : k.lastIndexOf('.');
return k.substring(startIndex, lastIndex) === component;
});
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0];
return dynamicViewsModules[matchKey];
} else if (matchKeys?.length > 1) {
warn(
'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure',
);
return;
} else {
warn('在src/views/下找不到`' + component + '.vue` 或 `' + component + '.tsx`, 请自行创建!');
return EXCEPTION_COMPONENT;
}
}
综合这两处代码,我们可以发现,asyncImportRoute
是一个递归调用,他的执行次数跟routes
的深度和数量有关,通过查询数据库了解到菜单的数量是6340,它和routes
是一一对应的关系,可以大致的判断asyncImportRoute
的递归次数不会少,并且在asyncImportRoute
中还调用了
item.component = dynamicImport(dynamicViewsModules, component as string);
代码对dynamicViewsModules
进行了遍历处理,它的数据量也是比较大的,所以一层递归加一层大数据量的循环下来,执行速度应该是快不起来的
那么解决问题思路可以从两方面入手:一方面是可以尽量减少菜单的数量,同时减少dynamicViewsModules
的数量;另一方面,在菜单和dynamicViewsModules
都无法减少的情况下,看看代码是否可以优化执行效率
经过代码的分析,不知道是幸运还是不幸,我发现了代码的优化空间,原来是有一处对象重复克隆导致了很大的耗时
let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
// Dynamic introduction
export function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
// 虽然这里的代码只执行了一次
dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../views/**/*.{vue,tsx}');
// 但是这下面的代码会重复执行在每次递归调用中
let subModules = import.meta.glob('../../../apps/**/src/index.{js,ts}', {
eager: true,
import: 'dynamicViewsModules',
});
for (const key in subModules) {
if (Object.prototype.hasOwnProperty.call(subModules, key)) {
const element = subModules[key];
// 在性能分析结果中可以发现这一行代码的耗时是最多的,占了20115毫秒
// Object.assign的执行效率是比较低的,而且这里还进行了重复的执行,因为他完全可以只执行一次(这里算是开发人员留下的一个bug了)
dynamicViewsModules = Object.assign({}, dynamicViewsModules, element);
}
}
所以我的优化思路是改为下面的代码
asyncImportRoute
方法体
let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
// Dynamic introduction
export function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
// 这里判断一下,只有dynamicViewsModules没有值的时候才执行下面的代码,保证下面的数组合并操作只执行一次
if (!dynamicViewsModules) {
dynamicViewsModules = import.meta.glob('../../views/**/*.{vue,tsx}');
let subModules = import.meta.glob('../../../apps/**/src/index.{js,ts}', {
eager: true,
import: 'dynamicViewsModules',
});
const modulesToMerge = Object.values(subModules).filter(Boolean);
// 将Object.assign合并为执行一次,避免重复执行造成的耗时
dynamicViewsModules = { ...dynamicViewsModules, ...Object.assign({}, ...modulesToMerge) };
}
dynamicImport
方法体
function dynamicImport(
dynamicViewsModules: Record<string, () => Promise<Recordable>>,
component: string,
) {
// 预处理 component 字符串
const componentKeys = new Set([
`../../views${component}.vue`,
`../../views${component}.tsx`,
`../../views/${component}.vue`,
`../../views/${component}.tsx`
])
for (const key of componentKeys) {
if (key in dynamicViewsModules) {
return dynamicViewsModules[key];
}
}
// 先不考虑这种情况的提示,来优化性能
// else if (Object.keys(dynamicViewsModules).some((key) => key.includes(component))) {
// warn(
// 'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure',
// );
// return;
// }
warn('在src/views/下找不到`' + component + '.vue` 或 `' + component + '.tsx`, 请自行创建!');
return EXCEPTION_COMPONENT;
}
优化后的asyncImportRoute
耗时
优化后的dynamicImport
,这里耗时并没有完全
可以看到优化后,页面可以在14.68秒完成加载,这已经是一个不小的提升,针对代码的提升就先进行到这。
剩余的问题就是考虑菜单是否有冗余数据,以及views目录下是否有冗余代码
菜单的冗余得业务上配合检查了,但是views下面扫描到的代码的情况,我们可以打断点看一下
我们复制Object.keys(dynamicViewsModules)
的值出来观察一下,这里可以发现好多的components
和data.ts
也被扫描到了,这样是不合理的,我们可以尝试修改import.meta.glob
匹配规则,也可以规范开发人员的开发方式来解决,这里就不多做处理了
[
"../../views/cockpit/components/CardTitle.vue",
"../../views/cockpit/components/CockpitSettingModal.vue",
"../../views/cockpit/components/CompanyInfo.vue",
"../../views/cockpit/components/Header.vue",
"../../views/cockpit/components/Map.vue",
"../../views/cockpit/components/SideView.vue",
"../../views/cockpit/components/viewCards/CommonCard.vue",
"../../views/cockpit/index.vue",
"../../views/commmod/messTmpl/index.vue",
"../../views/commmod/messTmpl/messTmplDetail.vue",
"../../views/commmod/messTmpl/messTmplModal.vue",
"../../views/dashboard/analysis/components/FacilitiesOverview.vue",
"../../views/dashboard/analysis/components/GrowCard.vue",
"../../views/dashboard/analysis/components/MainProject.vue",
"../../views/dashboard/analysis/components/ProductionStatistics.vue",
"../../views/dashboard/analysis/components/SalesProductPie.vue",
"../../views/dashboard/analysis/components/SiteAnalysis.vue",
"../../views/dashboard/analysis/components/ThreeLists.vue",
"../../views/dashboard/analysis/components/VisitAnalysis.vue",
"../../views/dashboard/analysis/components/VisitAnalysisBar.vue",
"../../views/dashboard/analysis/components/VisitRadar.vue",
"../../views/dashboard/analysis/components/VisitSource.vue",
"../../views/dashboard/analysis/components/announcements.vue",
"../../views/dashboard/analysis/components/carousel.vue",
"../../views/dashboard/analysis/components/reservation.vue",
"../../views/dashboard/analysis/home.vue",
"../../views/dashboard/analysis/index.vue",
"../../views/dashboard/helper/index.vue",
"../../views/dashboard/workbench/components/CommonFunction.vue",
"../../views/dashboard/workbench/components/CommonLink.vue",
"../../views/dashboard/workbench/components/DynamicInfo.vue",
"../../views/dashboard/workbench/components/ModuleCard.vue",
"../../views/dashboard/workbench/components/QuickNav.vue",
"../../views/dashboard/workbench/components/SaleRadar.vue",
"../../views/dashboard/workbench/components/WorkList.vue",
"../../views/dashboard/workbench/components/WorkListDetail.vue",
"../../views/dashboard/workbench/components/WorkbenchHeader.vue",
"../../views/dashboard/workbench/components/schedule.vue",
"../../views/dashboard/workbench/index.vue",
"../../views/dbcockpit/components/CardTitle.vue",
"../../views/dbcockpit/components/ContentBox.vue",
"../../views/dbcockpit/components/Dashboard.vue",
"../../views/dbcockpit/components/Header.vue",
"../../views/dbcockpit/components/ProgressBar.vue",
"../../views/dbcockpit/components/QualityItem.vue",
"../../views/dbcockpit/components/viewCards/DashboardCard.vue",
"../../views/dbcockpit/components/viewCards/DataGatherCard.vue",
"../../views/dbcockpit/components/viewCards/DataStorageCard.vue",
"../../views/dbcockpit/components/viewCards/GatherDutyCard.vue",
"../../views/dbcockpit/components/viewCards/GatherDutyMonitorCard.vue",
"../../views/dbcockpit/components/viewCards/GatherDutyRateCard.vue",
"../../views/dbcockpit/components/viewCards/QualityCheckCard.vue",
... 此处省略5000++
]