一次 SPA 架构下的性能优化实践

接收反馈

在最近我们陆续接到一些用户反馈:

"页面感觉卡卡的,转圈圈时间有点长。"

作为前端开发者,这类反馈再常见不过。但它看似模糊,背后却往往隐藏着架构层面的深层问题。这次我决定趁着业务相对空档,彻底剖析并优化我们系统的初始化性能问题。

为什么会 卡卡的?

我们的项目是一个标准的 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获取对应页面的配置, 具体逻辑代码实现就不列举了

相关推荐
小小小小宇23 分钟前
前端PerformanceObserver使用
前端
zhangxingchao1 小时前
Flutter中的页面跳转
前端
烛阴2 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝3 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇3 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军3 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加4 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript
Carlos_sam5 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖5 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
wordbaby6 小时前
React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析
前端·react.js