首页 编程开发 前端 正文

实战记录:一次SPA架构下的性能优化实践

接收反馈

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

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

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

为什么会 卡卡的?

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

非特殊说明,本文由99开发网(www.99kaifa.vip)原创或收集发布,技术无价旨在分享。

相关推荐

发布评论