接收反馈
在最近我们陆续接到一些用户反馈:
"页面感觉卡卡的,转圈圈时间有点长。"
作为前端开发者,这类反馈再常见不过。但它看似模糊,背后却往往隐藏着架构层面的深层问题。这次我决定趁着业务相对空档,彻底剖析并优化我们系统的初始化性能问题。
为什么会 卡卡的?
我们的项目是一个标准的 SPA(Single Page Application) 应用,初期设计为了保持统一性,所有模块共用同一入口和配置初始化逻辑,包括给外部用户分享的仪表盘等子模块。
但经过实际分析,我们发现一个严重的问题:
初始加载时,统一入口会发送几十甚至上百个配置类 HTTP 请求**,这些请求多数对当前模块其实是无效的!
具体来说:
- 用户只打开一个嵌套的仪表盘页面,但整个 SPA 初始化后会全量请求所有模块配置。
- 造成网络资源浪费 、JS 主线程长时间阻塞。
- 仪表盘模块虽然功能简单,但加载体验却比主模块还"重"。
初步分析:想改?代价太高!
一开始我们试图直接对配置模块进行"精简改造",结果发现:
- 配置模块与各业务模块深度耦合,理清依赖链工作量极大。
- 配置项间有交叉依赖,难以简单裁剪或懒加载。
- 要做到彻底精细化请求,需要大规模重构初始化流程。
彻底重构?显然不是当前阶段的最佳选择。
于是我们退而求其次,选择更务实的优化方案:
优化方案:从 SPA 到 MPA 的渐进式重构
在权衡收益与开发成本之后,我制定了如下两步走的优化方案:
1. 独立模块拆分为多页入口(SPA → MPA)
- 将原先作为 SPA 子路由存在的"仪表盘模块",抽离为 独立页面入口。
- 借助 webpack配置,将其打包为独立 JS Chunk,不再加载与主模块无关的代码。
这一步实现了两个好处:
- 首次加载资源明显减少(只加载当前模块所需 JS 和 CSS)。
- 页面逻辑更加清晰隔离,便于按模块优化。
具体webpack配置:
js
应用入口:
const ENTRY_JS = './src/app/*/*.js'
const path = require('path')
// 获取多应用入口文件
function getEntries(globPath) {
const files = glob.sync(globPath);
const entries = {};
let dirname;
let basename;
let pathname;
let extname;
files.forEach((item) => {
dirname = path.dirname(item); // 当前目录
extname = path.extname(item); // 后缀
basename = path.basename(item, extname); // 文件名
pathname = path.join(dirname, basename); // 文件路径
if (extname === '.html') {
entries[pathname] = item;
} else if (extname === '.js') {
entries[basename] = item;
}
});
return entries;
}
js
// htmlPlugin插件
function buildHtmlPlugins(globPath) {
const htmlPlugins = [];
const files = glob.sync(globPath);
const pageChunk = {
main:['vendors', 'common-vendor', 'chunk-main']
dashboard:'vendors', 'common-vendor', 'chunk-dashboard']
}
files.forEach((item) => {
const filename = path.basename(item, path.extname(item));
const conf = {
filename: `${filename}.html`,
template: './src/index.html',
hash: true,
chunks: [...pageChunk[filename] || [], filename],
minify: {
removeAttributeQuotes: true,
removeComments: true,
}
};
htmlPlugins.push(new HtmlWebpackPlugin(conf));
});
return htmlPlugins;
}
js
module.exports = {
entry: getEntries(ENTRY_JS),
output: {
path: 'dist',
filename: 'js/[name]_[chunkhash].js'
},
pugins: [
...buildHtmlPlugins(ENTRY_JS),
],
optimization: {
splitChunks: {
cacheGroups: {
// 具体chunk拆分参考https://webpack.docschina.org/configuration/optimization/
}
}
}
}
应用入口:index.js
js
------index.js---
export default (pageComponent) => {
new Vue({
el: '#root',
template: '<pageComponent/>',
components: { pageComponent },
});
};
dashboard入口:
js
--dashboard.js--
import init from '../../index.js';
import page from './dashboard.vue';
init(page);
---dashboard.vue--
<template>
<router-view />
</template>
<script>
export default {
name: 'dashboard'
};
</script>
main入口:
js
--main.js
import init from '../../index.js';
import page from './main.vue';
init(page);
---main.vue
<template>
<router-view />
</template>
<script>
export default {
name: 'main'
};
</script>
最终目录结构:
js
├── index.html
├── index.js
├── app
│ ├── dashboard
│ │ ├── xxx
│ │ ├── dashboard.js
│ │ ├── dashboard.vue
│ ├── main
│ │ ├── xxx
│ │ ├── main.js
│ │ └── main.vue
2. 配置信息按需请求(全量 → 精细化)
- 抽象出"当前模块所需配置类型",页面初始化时仅请求对应内容。
- 配置模块服务端也增加参数过滤支持,避免多余计算与网络传输。
- 后续计划支持"配置缓存",进一步降低重复请求。
实体实现: 新增一个dashboard模块下对应的页面相关的配置id的API,当访问具体的仪表盘时按id获取对应页面的配置, 具体逻辑代码实现就不列举了