Skip to content

甘肃托育三端 webpack 构建优化记录

日期:2026-08-14 范围:pc(政府端)/ medicine_pc(医疗端)/ sass(SaaS 平台端) 技术栈:vue-cli 4.4.6(webpack 4)+ Vue 2 + Element-UI


一、背景与诉求

  • 政府/医疗端构建产物碎片 chunk 太多(近 40+,每个几十 KB),大量串行排队
  • 领导要求:
    1. 合并碎片 chunk
    2. 登录页请求控制在 7-8 个
    3. 构建时预压缩(gzip),降低请求时服务器实时压缩开销

二、排查过程(基线分析)

对 pc 跑 vue-cli-service build --mode prod.gs --report,基线结论:

指标基线值
JS 总数87 个
总体积16 MB
登录页首屏 initial3 个(chunk-libs + chunk-elementUI + app)
chunk-libs8327 KB(8.1 MB) ← 真正的瓶颈
gzip 预压缩0 个

关键认知(纠正了最初的判断)

  1. 登录页首屏只有 3 个请求,不是 40。所谓"40 个碎片串行"是访问业务页时按需加载路由 chunk 的现象,不是登录页首屏。
  2. 真正的瓶颈是 chunk-libs 单文件 8.1 MB(登录页首屏光这一个文件就 8 MB)。
  3. 路由用 (resolve) => require(["@/views/xxx"], resolve) 旧式异步,96 条路由 → 96 个路由 chunk(碎片来源)。
  4. 路由碎片配置层合并不了require 异步每条路由一个 chunk,splitChunks 只能提取公共依赖、合并不了独立路由 chunk。
  5. @/views 共 248 个 .vue,constantRoutes 用 64 个,剩 184 个是 loadView 动态路由(后端菜单)。

三、配置层优化(三端一致)

1. splitChunks 重构

js
config.optimization.splitChunks({
  chunks: 'all',
  maxInitialRequests: 6,   // ⚠️ 默认 3 太低,会阻止 vendor 分组(见下方踩坑)
  maxAsyncRequests: 6,
  minSize: 30000,          // 默认值(30KB)
  maxSize: 0,              // 默认值(0=不二次拆分)
  cacheGroups: {
    elementUI: { test: /element-ui/,                priority: 25 },         // element-ui
    echarts:   { test: /(echarts|zrender)/,         priority: 20 },         // 图表(async 公共 vendor)
    docs:      { test: /(xlsx|jspdf|pdfjs-dist)/,   priority: 20 },         // 文档/导出大库
    libs:      { test: /node_modules/, chunks:'initial', priority: 10 },    // 兜底 initial node_modules
    commons:   { test: resolve('src/components'), minChunks: 2, priority: 5 }
  }
})

分组原则:只给 echarts/xlsx/jspdf 这类大库单独分组,小库(quill/vue-core 几百 KB)合并进 libs 兜底——否则分组太多会占满 initial 名额、挤掉 libs

2. gzip 预压缩

  • pc/medicine 补装 compression-webpack-plugin@6.1.2(webpack 4 兼容版,sass 已有)
  • 配置:threshold: 51200(50 KB)、只压 js/css/html/svgdeleteOriginalAssets: false

3. 修 sass 错误配置

  • minSize: 1000000(1 MB 太大,导致 vendor 提取失效、被打散进各路由)
  • test 去掉图片(jpg/png/gif 本身已压缩,gzip 无收益还多生成无用 .gz

⚠️ 踩坑:maxInitialRequests 默认 3

webpack 4 的 maxInitialRequests 默认是 3(entry 首屏并发上限)。当 cacheGroup 分组数 + app 超过 3,低 priority 的分组被放弃,模块全塞回 app.js

  • 现象:splitChunks 加了多个分组后,app.js 从 489 KB 暴涨到 8 MB,chunk-libs 不生成
  • 正解:maxInitialRequests 提到覆盖分组数的值(本项目用 6)

四、最终数据

app.jschunk-libs.gz 数(改前→改后)首屏 initial
pc 政府端492 KB6952 KB54 → 125
medicine 医疗端476 KB7048 KB17 → 115
sass SaaS 端1412 KB7452 KB17 → 75

成果

  • gzip 预压缩:三端大文件全部 .gz,首屏 vendor(app/elementUI/echarts/docs/libs)全覆盖,省服务器实时压缩 CPU
  • vendor 分离:echarts/docs/elementUI 独立 chunk,业务发版不连带失效这些库缓存
  • app.js 瘦:业务代码与 vendor 分离(pc 492 KB / medicine 476 KB)
  • threshold 50 KB.gz 数大减(只压大文件),<50 KB 小文件走 nginx 实时 gzip(开销可忽略)
  • 登录页 6 个请求(5 initial + login chunk)

五、部署必做

nginx 配 gzip_static on;——不配的话构建出的 .gz 不会被发送,nginx 仍走实时压缩,预压缩白做。

nginx
gzip_static on;

六、评估后放弃的(没做,记录原因)

1. 路由合并(方案 A:require → import + webpackChunkName)

  • 实测引入问题() => import() 在 vue-cli 4 + webpack 4 这套里异常合并——74 个路由 import 被合成 2 个巨型 chunk(1.67 MB + 276 KB),且 webpackChunkName: "group-xxx" 注释完全失效(chunk 名是 hash 不是 group-),app.js 暴涨 1.6 MB
  • 已干净回退(router/index.js 恢复 require,重建确认 app 492 KB / 87 js / 54 gz 全恢复)
  • 结论require 是这个项目唯一能正常代码分割的写法,不要改 import(babel dynamic-import-node 只在 dev 启用,生产仍异常,根因疑似 vue-cli4+webpack4 对 import(/* chunkName */) 的处理问题)

2. loadView 动态路由合并

  • loadView = (view) => require([\@/views/${view}`])`,路径是后端返回的变量
  • /* webpackChunkName */ 只认静态字符串 + [request] 占位符,不能用变量 → 184 个动态路由页面注定各自独立 chunk,合不了

3. echarts 改 async(真正减首屏体积)

  • echarts 被 @/components/Echarts 全局组件引用 → 注定 initial(首屏加载),配置层 chunks:'async' 无效(实测退回 chunk-libs)
  • 要减首屏必须动业务代码(全局组件改懒加载)
  • ⚠️ 更正:docs(xlsx/jspdf)后来实测 chunks:'async' 出首屏(只被 2 个页面引用,属"少 chunk"情况);echarts / quill / @vue-office(多页面共享)才不能——详见第八章「各 SDK 能否 async」结论表

七、经验沉淀

maxInitialRequests 默认 3 的坑 + splitChunks 默认值速查 + gzip 配置要点,已写入 ~/.claude/rules/engineering.md 的「splitChunks 分组与 maxInitialRequests 陷阱(webpack4)」小节,避免以后重蹈。


八、深挖第二个问题:chunk-libs 拆分 / @vue-office 不进登录页(2026-08-14 续)

背景

领导进一步要求:chunk-libs(7.6M,首屏大头)继续拆,让 @vue-office 等大 SDK 不进登录页,每个路由只加载自己需要的代码。

排查:chunk-libs 大头到底是谁

vue-cli 的 --report(webpack-bundle-analyzer)只分析 src,不含 node_modules——chunk-libs 全是 node_modules,report 根本看不到。

改用 webpack stats:vue.config.js 临时加一个 inline plugin,构建时把 stats.toJson() 写成 stats.json,再 node 分析 chunk-libs 的模块:

js
new (class WriteStats {
  apply(compiler) {
    compiler.hooks.done.tap('WriteStats', stats => {
      require('fs').writeFileSync('stats.json', JSON.stringify(
        stats.toJson({ all: false, chunks: true, modules: true, chunkModules: true, source: false })
      ));
    });
  }
})()

chunk-libs(7656KB)真实构成 top:

模块体积
@vue-office/pdf2732 KB
@vue-office/excel1629 KB
@vue-office/pptx1345 KB
@vue-office/docx171 KB
@vue-office 合计5877 KB(chunk-libs 的 77%)
html2canvas431 KB
quill429 KB
其余(vue-core/jquery/vue-amap/...)~920 KB

@vue-office 全家桶(文件预览)是首屏大头,登录页根本用不到。 它通过 @/views/components/filePreview 全局注册进了 main.js 链路 → initial。

五种尝试(记录成败,避免重试)

#方案结果
1filePreview 整体异步注册Vue.component('filePreview', () => import(...))❌ @vue-office 没出 chunk-libs
2filePreview 内部 @vue-office 改异步组件components: { VueOfficeDocx: () => import('@vue-office/docx') }❌ webpack4 不识别 Vue SFC 同步注册组件里的 components import(),@vue-office 还在 chunk-libs
3filePreview 取消全局、21 个页面改局部 import⚠️ filePreview 组件出首屏(小赚),但 @vue-office 还是没出
4vueOffice cacheGroup chunks:'all'(priority 30)⚠️ @vue-office 出 chunk-libs 了(7656→1812),但单独的 chunk-vue-office(5848)进了首屏
5vueOffice cacheGroup chunks:'async'❌ @vue-office 退回 chunk-libs(7656),chunk-vue-office 根本没生成

根因:webpack4 对 node_modules 的硬限制

webpack4 splitChunks 对 node_modules vendor 有强制 initial 兜底:

  • chunks:'all' → 提取出来的 vendor chunk 算首屏
  • chunks:'async'拒绝提取,退回 chunk-libs

两条路都出不了首屏。webpack5 才修了 chunks:'async' 对 node_modules 的支持。

关键区别:路由的 () => import("@/views/...")src)能正常分割(产物 80+ chunk),但全局注册组件里的 () => import('node_modules/...')(node_modules)不行——webpack4 对 src vs node_modules 的动态 import 区别对待。

结论:各 SDK 到底能不能改 async(不首屏)

基于本次实测(webpack4 / vue-cli 4.4.6):

SDK引用方式webpack4 能 async?原因
docs(xlsx/jspdf)2 个页面用(少 chunk)chunks:'async' 成功提取、出首屏
echarts17 个图表组件用(多页面共享)不能chunks:'async' 退回 chunk-libs(多 chunk 共享,vendors 兜底 initial)
quillEditor 全局注册(多页面用)不能Editor 改异步也没用,quill 退回 chunk-libs
@vue-officefilePreview 全局 + 21 页面不能5 种方式全失败(整体异步/组件异步/局部注册/chunks:all/chunks:async)

通用规则(webpack4,实测)

  • node_modules 大库被全局组件多页面共享引用 → 拆不出首屏chunks:'all' 算首屏 / chunks:'async' 退回 chunk-libs)
  • node_modules 大库被少数 chunk引用 → chunks:'async' 可出首屏(如 docs)
  • src 模块(路由 () => import("@/views/..."))→ 正常分割,不受此限制

一句话:webpack4 下,凡是"全局注册组件里的 node_modules 大库"(echarts/quill/@vue-office 这类),配置层和业务层都拆不动它出首屏。要让它们真正按需加载,唯一出路是升级 webpack5chunks:'async' 对 node_modules 生效,不管共享数)。

最终方案(webpack4 下的折中)

sass 接受 @vue-office 首屏(拆不动),保留:

  • filePreview 改 21 页面局部注册(filePreview 组件跟着页面走、出首屏,$refs.filePreview.open() 不受影响,真机验证 OK)
  • vueOffice cacheGroup 删除(不让 chunk-vue-office 进首屏)
  • @vue-office 留在 chunk-libs 首屏,但 gzip 已把传输砍 72%(7656→2179KB)

pc/medicine 同病同因(filePreview 也全局注册、@vue-office 也首屏),领导说只搞 sass,这俩没动。

三端实测(领导验证,修复前后)

请求数已传输加载时间最大单文件
政府端 pc5911.3 M2.1 s2.8 M
2112.3 M1.6 s2.8 M
机构端 sass1811.8 M2.8 s8.9 M
1911.9 M2.0 s7.8 M
医疗机构 medicine6611.4 M1.9 s2.8 M
2514.4 M2.3 s7.2 M

观察

  • pc/medicine 请求数大减(59→21、66→25):threshold 50KB + splitChunks vendor 分离,首屏请求结构精简
  • sass 加载时间明显快(2.8→2.0s):gzip 预压缩省了服务器实时压缩开销
  • @vue-office 首屏体积未减(webpack4 限制),但 gzip 已减传输
  • medicine 最大单文件 2.8→7.2M:splitChunks 后 chunk-libs 集中了 vendor(之前分散)

唯一真正让 @vue-office 出首屏的出路

升级 webpack5chunks:'async' 对 node_modules 生效)。大工程,需评估。


九、webpack5 升级评估(让 echarts/quill/@vue-office 出首屏的唯一出路)

为什么要考虑升级

第八章结论:webpack4 下 echarts/quill/@vue-office(全局组件里的 node_modules 大库)拆不出首屏。webpack5 修了 chunks:'async' 对 node_modules 的支持,才能让它们真正按需加载。

升级路径

vue-cli4 绑死 webpack4,不能单独换。三条路:

路径改动量保留 Vue 2.6?推荐度
vue-cli5(@vue/cli-service 5,内置 webpack5)✅ 保留⭐ 推荐
Vite(替代 webpack)大(重写构建)需 vite-plugin-vue2高风险
手动 webpack5(脱离 vue-cli)最大不推荐

改动点(vue-cli5 路径)

  1. 依赖升级:@vue/cli-service + @vue/cli-plugin-* 4.4.6 → 5.x;webpack 4 → 5;sass-loader 10 → 12+;compression-webpack-plugin 6 → 10+(6.x 只支持 webpack4)
  2. Node polyfill(最大坑):webpack5 不再自动 polyfill node 核心(crypto/buffer/process/path)。项目里 import crypto from 'crypto'(pc login.vue)、crypto-js/jsencrypt 等若用到 node API,要手动配 resolve.fallback + 装 polyfill 包(crypto-browserify 等),逐个验证
  3. vue.config.js breakingdevServer.beforesetupMiddlewares;chainWebpack 部分 API 变
  4. splitChunks:webpack5 chunks:'async' 对 node_modules 生效(目标达成)+ maxAsyncRequests 默认 5 → 30
  5. asset modules:file-loader/url-loader → webpack5 asset modules(vue-cli5 兼容,基本透明)

改动量:中到大

  • 依赖升级 + npm i 重装
  • Node polyfill 配置(crypto 包 / crypto-js / jsencrypt 逐个验)
  • vue.config.js 适配(devServer / chainWebpack)
  • 三端全功能回归:构建 + 运行时,加密(登录)/上传/图表/地图/文件预览全过一遍

主要风险

  • Node polyfillcrypto 包 / crypto-js 报错(最大不确定性,必须实测)
  • 插件/loader 版本不兼容(html-webpack-plugin 5、compression-webpack-plugin 10 等)
  • 运行时功能挂(polyfill 缺失导致加密/解密挂)

收益

  • ✅ echarts/quill/@vue-office 真正出首屏(本次目标达成)
  • 构建更快(webpack5 持久缓存 filesystem)
  • 包更小(tree-shaking 改进、未用代码剔除更狠)
  • 长期维护(webpack4 已 EOL,vue-cli4 停维)

建议

  • 不建议为"SDK 出首屏"单独升级:改动大、风险高,而 gzip 已把 chunk-libs 传输砍 72%(7656→2179KB),首屏实际传输可控——ROI 不值
  • 若项目要长期维护 + 上 webpack5 生态:可规划 vue-cli5 升级,排期 Node polyfill 验证 + 全功能回归(粗估 2~5 人天,看 polyfill 坑的深浅)
  • 短期:保持 webpack4 + 接受 @vue-office 首屏,是当前性价比最高的选择

十、gzip 三层配合 + 浏览器怎么判断生效

三个角色(别混淆)

角色什么时候干啥谁的 CPU
CompressionPlugin(webpack 构建)打包时把 >50KB 的 js/css 压成 .gz 放产物构建机(一次性)
nginx gzip_static on(请求时)收到请求同目录有 .gz 就直发它(用现成的)
nginx gzip on(请求时兜底)收到请求.gz 的文件实时压服务器(小文件,开销小)

nginx 必须配两条(缺一不可)

nginx
gzip_static on;   # 有 .gz 直发(预压缩,省 CPU) ← 不配则 .gz 白生成
gzip on;          # 兜底:没 .gz 的(小文件)实时压

两种错误配法

  • gzip on、没 gzip_static on:CompressionPlugin 生成的 .gz 白生成,nginx 每次请求仍实时压原文件 → "省实时压缩 CPU"目标达不成(最常踩的坑
  • 只 CompressionPlugin、nginx 啥都不配.gz 不发、原文件也不压 → 大文件全量传输,首屏爆炸

浏览器怎么判断 gzip 生效(content-Encoding)

基础判断(传输有没有压缩)

  • F12 → Network → 点任一 js/css → Response Headers → 看到 Content-Encoding: gzip = 该响应被 gzip 压缩传输(首屏传输减 70%+)
  • 只要看到 content-Encoding: gzip,就说明浏览器收到的是压缩版、gzip 这层优化生效了

content-Encoding: gzip 区分不了

  • 预压缩(gzip_static 发 .gz)vs 实时压缩(gzip 现场压)——两种响应头都是 content-Encoding: gzip,浏览器看上去完全一样
  • 即:光看响应头,无法知道服务器是"发预压缩的 .gz"还是"现场压的"

要确认 gzip_static(预压缩)真生效(服务器省 CPU)

  1. Content-Length ≈ 产物里 .gz 文件的大小(gzip_static 直发 .gz,大小一致)
  2. 或看服务器 CPU 占用(gzip_static 低、实时 gzip 高)
  3. 或 nginx 加自定义 add_header X-Gzip-Static hit;(手动标记,gzip_static 自身不发)

一句话:浏览器 content-Encoding: gzip 只能确认"首屏传输减了"(用户侧收益到手);服务器到底省没省 CPU(gzip_static vs 实时)得看服务器端配置和负载。


附:splitChunks 默认值速查(webpack 4)

参数默认值含义
chunks'async'默认只对懒加载 chunk 生效
minSize30000(30 KB)低于此不单独提取、并入父 chunk
maxSize00 = 不二次拆分(maxSize 是"超过就拆",与合并相反)
minChunks1至少被几个 chunk 引用才提取
maxAsyncRequests5异步加载最大并发请求数
maxInitialRequests3入口首屏最大并发请求数(本项目提到 6)

© 2026 开发速查