Przeglądaj źródła

perf(web): 降低生产构建内存峰值,并启用 CI 低内存打包模式

关闭按需 Element Plus 样式与构建期 dts、限制 rollup 并行、CI 跳过 gzip;
新增 npm run build:ci,供 Drone 在小内存机器上打包。

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 6 dni temu
rodzic
commit
9d93a373ae

+ 6 - 3
.drone.yml

@@ -37,17 +37,20 @@ steps:
 
   - name: package-web
     # 无 package-lock(被 gitignore);Node20 需 --legacy-peer-deps 规避 npm edgesOut
+    # 低内存模式:串行 transform、关 gzip 插件、3G 堆(见 web build:ci)
     image: node:20-bookworm
     environment:
       NPM_CONFIG_REGISTRY: https://registry.npmmirror.com
+      CI: 'true'
+      VITE_LOW_MEM: '1'
+      VITE_BUILD_COMPRESS: ''
     commands:
       - set -eu
-      - echo "==> web tag=${DRONE_TAG} npm=$(npm -v) node=$(node -v)"
+      - echo "==> web tag=${DRONE_TAG} npm=$(npm -v) node=$(node -v) low_mem=${VITE_LOW_MEM}"
       - mkdir -p "/hzdyjs-releases/${DRONE_TAG}"
       - cd web
       - npm install --legacy-peer-deps --no-audit --no-fund
-      # 本前端生产构建实测需约 6G+ 堆;测试机靠扩容 swap(/swap-hzdyjs)硬扛
-      - node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode production
+      - npm run build:ci
       - test -f dist/index.html
       - tar -C dist -czf "/hzdyjs-releases/${DRONE_TAG}/web-${DRONE_TAG}.tar.gz" .
       - ls -lh "/hzdyjs-releases/${DRONE_TAG}/web-${DRONE_TAG}.tar.gz"

+ 3 - 2
web/package.json

@@ -8,8 +8,9 @@
   "type": "module",
   "scripts": {
     "dev": "vite serve --mode development",
-    "build:prod": "node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode production",
-    "build:dev": "node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode development",
+    "build:prod": "node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode production",
+    "build:ci": "CI=true VITE_LOW_MEM=1 VITE_BUILD_COMPRESS= node --max-old-space-size=3072 node_modules/vite/bin/vite.js build --mode production",
+    "build:dev": "node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode development",
     "preview": "vite preview",
     "lint:eslint": "eslint",
     "lint:eslint:fix": "eslint --fix",

+ 22 - 3
web/vite.config.ts

@@ -5,6 +5,9 @@ import path from 'path';
 
 export default defineConfig(({ mode, command }) => {
   const env = loadEnv(mode, process.cwd());
+  const isBuild = command === 'build';
+  // CI / 小内存机器:限制并行、跳过 gzip 插件(产物外层已 tar.gz)
+  const lowMem = process.env.CI === 'true' || process.env.VITE_LOW_MEM === '1';
   return {
     // 部署生产环境和开发环境下的URL。
     // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
@@ -17,7 +20,7 @@ export default defineConfig(({ mode, command }) => {
       extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
     },
     // https://cn.vitejs.dev/config/#resolve-extensions
-    plugins: createPlugins(env, command === 'build'),
+    plugins: createPlugins(env, isBuild),
     server: {
       host: '0.0.0.0',
       port: Number(env.VITE_APP_PORT),
@@ -54,6 +57,23 @@ export default defineConfig(({ mode, command }) => {
         ]
       }
     },
+    esbuild: {
+      legalComments: 'none'
+    },
+    build: {
+      sourcemap: false,
+      // 控制台 gzip 体积统计会再压缩一遍全部产物,大项目很吃内存
+      reportCompressedSize: false,
+      chunkSizeWarningLimit: 2000,
+      // esbuild 比 terser 省内存、更快
+      minify: 'esbuild',
+      cssMinify: 'esbuild',
+      target: 'es2020',
+      // 小内存 / CI:串行读文件,显著降低 transform 峰值
+      rollupOptions: {
+        maxParallelFileOps: lowMem ? 1 : 2
+      }
+    },
     // 预编译
     optimizeDeps: {
       include: [
@@ -65,8 +85,7 @@ export default defineConfig(({ mode, command }) => {
         'echarts',
         'vue-i18n',
         '@vueup/vue-quill',
-        'image-conversion',
-        'element-plus/es/components/**/css'
+        'image-conversion'
       ]
     }
   };

+ 5 - 4
web/vite/plugins/auto-import.ts

@@ -1,20 +1,21 @@
 import AutoImport from 'unplugin-auto-import/vite';
 import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
 
-export default (path: any) => {
+export default (path: any, isBuild = false) => {
   return AutoImport({
     // 自动导入 Vue 相关函数
     imports: ['vue', 'vue-router', '@vueuse/core', 'pinia'],
     eslintrc: {
-      enabled: true,
+      // 生产构建不重写 eslint 全局声明,避免每次打包全量扫描
+      enabled: !isBuild,
       filepath: './.eslintrc-auto-import.json',
       globalsPropValue: true
     },
     resolvers: [
       // 自动导入 Element Plus 相关函数ElMessage, ElMessageBox... (带样式)
-      ElementPlusResolver()
+      ElementPlusResolver({ importStyle: false })
     ],
     vueTemplate: true, // 是否在 vue 模板中自动导入
-    dts: path.resolve(path.resolve(__dirname, '../../src'), 'types', 'auto-imports.d.ts')
+    dts: isBuild ? false : path.resolve(path.resolve(__dirname, '../../src'), 'types', 'auto-imports.d.ts')
   });
 };

+ 5 - 3
web/vite/plugins/components.ts

@@ -2,16 +2,18 @@ import Components from 'unplugin-vue-components/vite';
 import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
 import IconsResolver from 'unplugin-icons/resolver';
 
-export default (path: any) => {
+export default (path: any, isBuild = false) => {
   return Components({
     resolvers: [
       // 自动导入 Element Plus 组件
-      ElementPlusResolver(),
+      // 全局已引入 element-plus/dist/index.css,关闭按需样式,减少打包模块数
+      ElementPlusResolver({ importStyle: false }),
       // 自动注册图标组件
       IconsResolver({
         enabledCollections: ['ep']
       })
     ],
-    dts: path.resolve(path.resolve(__dirname, '../../src'), 'types', 'components.d.ts')
+    // 生产构建不重写声明文件,避免把全部组件信息留在内存里
+    dts: isBuild ? false : path.resolve(path.resolve(__dirname, '../../src'), 'types', 'components.d.ts')
   });
 };

+ 8 - 4
web/vite/plugins/compression.ts

@@ -1,16 +1,20 @@
 import compression from 'vite-plugin-compression';
 
 export default (env: any) => {
-  const { VITE_BUILD_COMPRESS } = env;
+  // CI 可通过空值或空格关闭:VITE_BUILD_COMPRESS=
+  const raw = String(env?.VITE_BUILD_COMPRESS ?? '').trim();
   const plugin: any[] = [];
-  if (VITE_BUILD_COMPRESS) {
-    const compressList = VITE_BUILD_COMPRESS.split(',');
+  if (raw) {
+    const compressList = raw.split(',').map((s: string) => s.trim()).filter(Boolean);
     if (compressList.includes('gzip')) {
       // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
       plugin.push(
         compression({
           ext: '.gz',
-          deleteOriginFile: false
+          deleteOriginFile: false,
+          // 小于 10KB 的分包压缩收益很小,跳过可明显缩短收尾时间
+          threshold: 10 * 1024,
+          verbose: false
         })
       );
     }

+ 3 - 3
web/vite/plugins/icons.ts

@@ -1,8 +1,8 @@
 import Icons from 'unplugin-icons/vite';
 
-export default () => {
+export default (isBuild = false) => {
   return Icons({
-    // 自动安装图标库
-    autoInstall: true
+    // 构建时禁止临时 npm install,避免打包过程访问外网卡住
+    autoInstall: !isBuild
   });
 };

+ 3 - 5
web/vite/plugins/index.ts

@@ -1,5 +1,4 @@
 import vue from '@vitejs/plugin-vue';
-import vueDevTools from 'vite-plugin-vue-devtools'; // 临时禁用以解决localStorage兼容性问题
 
 import createUnoCss from './unocss';
 import createAutoImport from './auto-import';
@@ -13,12 +12,11 @@ import path from 'path';
 export default (viteEnv: any, isBuild = false): [] => {
   const vitePlugins: any = [];
   vitePlugins.push(vue());
-  // vitePlugins.push(vueDevTools()); // 禁用以解决localStorage兼容性问题
   vitePlugins.push(createUnoCss());
-  vitePlugins.push(createAutoImport(path));
-  vitePlugins.push(createComponents(path));
+  vitePlugins.push(createAutoImport(path, isBuild));
+  vitePlugins.push(createComponents(path, isBuild));
   vitePlugins.push(createCompression(viteEnv));
-  vitePlugins.push(createIcons());
+  vitePlugins.push(createIcons(isBuild));
   vitePlugins.push(createSvgIconsPlugin(path));
   vitePlugins.push(createSetupExtend());
   return vitePlugins;