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