vite3资源打包优化

前言

最近做的需求是海外的项目,使用的 vite2.xvue3 来开发,在第一版提测的时候,发现打包之后,总包大小有 4M 多,有很大的优化的空间。

下面我将从以下几个维度给大家讲下如何优化一个vite项目的:👇🏻

工具链说明

  1. 目前 vite2.x 是基于 rollup 打包的,而不是 esbuild,详见这里
  2. 使用 rollup-plugin-visualizer 进行打包分析,打包之后,会在根目录默认生成一个 stats.html 文件
  3. 使用 manualChunks 对主包行配置分割,包括 cssjs 文件(核心工具包和第三方依赖包)等
  4. 使用 vite-plugin-imagemin 进行图片压缩。每次打包都会压缩一次,会占用构建耗时,有值得优化的空间,此处先不作讨论
  5. @vitejs/plugin-legacy vite 默认对浏览器支持的基线是支持 ESM 的现代浏览器,这个插件就是用来兼容不支持 ESM 的浏览器,详见这里

vite.config.ts 配置如下:👇🏻

ts 复制代码
const { resolve } = require('path');
const fs = require('fs');
import { ConfigEnv, defineConfig, loadEnv, UserConfigExport } from 'vite';
import { createVitePlugins } from './build/vite/plugins';
import { createProxy } from './build/vite/proxy';
import { wrapperEnv } from './build/utils';
import externalGlobals from 'rollup-plugin-external-globals';
function pathResolve(dir: string) {
  return resolve(__dirname, dir);
}
/**
 * 根据环境来读取配置文件(本地环境和对应的环境)
 */
function configEnvFile(mode: ConfigEnv['mode'], isOverseaEnv: boolean) {
  const dotenv = require('dotenv');
  if (mode) {
    const NODE_ENV = mode;
    if (isOverseaEnv) {
      const dotenvFiles = [`.env.${NODE_ENV}.sea`].filter(Boolean);
      // 遍历文件注入所有环境变量(这里就把配置文件的变量注入到process.env中了,只是node环境可访问)
      dotenvFiles.forEach((dotenvFile) => {
        if (fs.existsSync(dotenvFile)) {
          dotenv.config({
            override: true,
            path: pathResolve(dotenvFile),
          });
        }
      });
    }
  }
}
const SPLIT_VENDOR_CONFIG = ['axios', 'vue-router', 'pinia', 'vue'];
/**
 * 详细配置:https://cn.vitejs.dev/config/
 */
export default ({ command, mode }: ConfigEnv): UserConfigExport => {
  const isOverseaEnv = process.env.LOCATION === 'sea';
  configEnvFile(mode, isOverseaEnv);
  const root = process.cwd();
  const env = loadEnv(mode, root);
  // 从 env 读取的boolean会被转换为string,重新转为boolean类型
  const viteEnv = wrapperEnv(env);
  const { VITE_SERVER_HOST: host = 'dev.game.data.woa.com' } = viteEnv;
  // const appTitle = process.env.APP_TITLE;
  // if (appTitle) viteEnv['VITE_GLOB_APP_TITLE'] = appTitle;
  const isBuild = command === 'build';
  // const prefix = `monaco-editor/esm/vs`;
  // 配置 Vite 依赖预编译,缩短项目冷启动时间
  const optimizeDeps = {
    include: [
      'vue',
      'vue-router',
    ],
  };
  const rollupOptions = {
    output: {
      chunkFileNames: 'assets/js/[name]-[hash].js',
      entryFileNames: 'assets/js/[name]-[hash].js',
      assetFileNames: 'assets/static/[name]-[hash].[ext]',
    },
    // 下面这些次要依赖,不需要打包,走旁路cdn的形式
    external: ['@antv/g2plot', '@antv/g6', '@antv/s2'],
    plugins: [
      externalGlobals({
        // vue: 'Vue',
        // echarts: 'echarts',
        // 'element-plus': 'ElementPlus',
        '@antv/g2plot': 'G2Plot',
        '@antv/g6': 'G6',
        '@antv/s2': 'S2',
      }),
    ],
  };
  /**
   * chunk 分隔策略
   * @param mapping 记录类型,包含键和值的字符串对
   * @param id id - 需要查询的目标ID字符串
   * @returns 
   */
  const mapManualChunks = (mapping: Record<string, string>, id: string): string | undefined => {
    // 使用for...of循环遍历映射对象的每一对键值对
    for (const [match, chunk] of Object.entries(mapping)) {
      // 检查当前ID是否包含匹配字符串(match)
      if (id.includes(match)) {
        // 如果找到匹配项,立即返回对应的块(chunk)
        return chunk;
      }
    }
    // 如果没有找到匹配项,则返回undefined
    return void 0;
  };

  /**
   * 手动分包策略,拆解 vendor 包
   * @param id
   * @returns
   */
  function manualChunks(id: string) {
    if (id.includes('.css')) {
      if (id.includes('iconfont')) return 'iconfont';
      if (id.includes('public')) return 'public';
    }
    // 拆包
    const chunkName = mapManualChunks({
      // 'vue': 'vue',
      'echarts': 'echarts',
      'jsplumb': 'jsplumb',
      'lodash-es': 'lodash-es',
      'element-plus': 'element-plus',
      'monaco-editor': 'monaco-editor',
      // 下面这些次要依赖,走旁路cdn的形式
      // '@antv/g2plot': 'g2plot',
      // '@antv/g6': 'g6',
      // '@antv/s2': 's2',
    }, id);
    if (chunkName) return chunkName;
    if (id.includes('node_modules')) {
      let chunk = '';
      SPLIT_VENDOR_CONFIG.forEach((item) => {
        if (id.includes(item)) {
          chunk = `${item}`;
        }
      });
      if (chunk !== '') return chunk;
      const depName = id.toString()?.split('node_modules/')[1]?.split?.('/')?.[0];
      if (depName) {
        return depName.toString();
      }
      return 'vendor';
    }
  }
  rollupOptions['output']['manualChunks'] = manualChunks;
  const alias = {
    '@': pathResolve('src'),
    vue$: 'vue/dist/vue.runtime.esm-bundler.js',
  };
  const proxy = createProxy(viteEnv);
  // vite 工程相关的插件. 数量多,抽离出去单独管理
  const plugins = createVitePlugins(viteEnv, isBuild);
  const esbuild = {
    jsxFactory: 'h',
    jsxFragment: 'Fragment',
    drop: ['console', 'debugger']
  };
  // 具体的的配置选项:https://vitejs.dev/config/#config-file
  return defineConfig({
    base: '/', // index.html文件所在位置
    root, // js导入的资源路径,src
    resolve: {
      alias,
      extensions: ['.js', '.ts', '.vue', '.tsx', '.json'],
    },
    define: {
      'process.env.APP_IS_LOCAL': '"true"',
    },
    server: {
      hmr: true,
      // 监听所有本地 IP
      //   host: true,
      // host: 'dev.game.data.woa.com',
      host,
      // 是否开启 https
      //   https: true,
      // 端口
      //   port: 1024,
      // 代理
      proxy,
    },
    build: {
      target: 'es2015',
      minify: 'esbuild', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser
      manifest: false, // 是否产出maifest.json
      sourcemap: false, // 是否产出soucemap.json
      outDir: 'dist', // 产出目录
      reportCompressedSize: false, // 启用/禁用 gzip 压缩大小报告。压缩大型输出文件可能会很慢,因此禁用该功能可能会提高大型项目的构建性能。
      rollupOptions,
      // terserOptions: {
      //   compress: {
      //     keep_infinity: true,
      //     // Used to delete console in production environment
      //     drop_console: true,
      //   },
      // },
      // 关闭 brotliSize 显示可以稍微减少打包时间
      brotliSize: false,
      // 限制最大包的大小
      chunkSizeWarningLimit: 2000,
      commonjsOptions: {
        include: [/node_modules/, /src/],
        exclude: [/monaco-editor\/esm/],
        extensions: ['.js', '.vue', '.cjs'],
        // 把monaco-editor从commonjs中exclude掉,不需要进行commonjs转化。
        transformMixedEsModules: true,
      },
      cssCodeSplit: true, //css 拆分
    },
    esbuild,
    optimizeDeps,
    // vite 工程相关的插件. 数量多,抽离出去单独管理
    // plugins: createVitePlugins(viteEnv, isBuild),
    plugins,
    css: {
      preprocessorOptions: {
        scss: {
          // 支持内联 JavaScript
          javascriptEnabled: true,
        },
      },
    },
    compilerOptions: {
      types: ['node', 'jest', 'vite/client'],
    },
  });
};

打包信息的输出如下:👇🏻

从打包输出信息我们可看出:

  1. assets/index.43ae9aaf.js 打包之后 14499.98 KiB / gzip 之后仍然有 3732.16 KiB
  2. 个别图片资源过大:
bash 复制代码
    dist/assets/banner.051258d3.png 1423.75 KiB
  1. 构建用了375.74秒,存在优化空间

一、配置路由懒加载

ts 复制代码
    import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

    const routes: RouteRecordRaw[] = [
      {
        path: "/",
        name: "Home",
        component: () => import("@/pages/Home.vue"),
      },
      {
        path: "/login",
        name: "Login",
        component: () => import("@/pages/Login.vue"),
      },
    ];

    const router = createRouter({
      history: createWebHistory(),
      routes,
    });

二、分析构建

安装rollup-plugin-visualizer插件,该插件用于分析依赖大小占比。

bash 复制代码
    npm install rollup-plugin-visualizer @types/rollup-plugin-visualizer -D

vite.config.ts中引入并使用它。

ts 复制代码
    import { visualizer } from "rollup-plugin-visualizer";

    export default defineConfig({
      // ...
      plugins: [
        // 将这个visualizer插件放到最后的位置中
        visualizer(),
      ],
    });

当你配置完毕之后,在你下一次npm build项目时,会在目录下创建一个stats.html,里面即是你项目中的分析结果。分析中你可以将空间占比比较大的文件进行适当的优化。

主包的体积达到了惊人的 27.61M

三、代码压缩

安装vite-plugin-compress插件,对项目中的代码进行gzip压缩或brotli压缩

bash 复制代码
    npm install vite-plugin-compress -s

vite.config.ts中引入并使用它。

ts 复制代码
    import compress from "vite-plugin-compress";

    export default defineConfig({
      // ...
      plugins: [compress()],
    });

四、优化包体积

vue3中,许多的Api都是可以被tree-shake优化,也即是你的项目中使用到了某些API只打包这些被使用到的API,减少包的体积。在选择第三方库时,尽量使用 ES 版本就比如lodash-eslodash,前者是ES6格式的代码可以被tree-shake,而lodash则是全部引入,体积较大。

兼容

目前,大部分的浏览器支持了ESM,但部分旧版浏览器不支持ESM,此时就需要使用@vitejs/plugin-legacy来兼容这些旧版的浏览器。详细请戳这里

安装@vitejs/plugin-legacy

bash 复制代码
    npm install @vitejs/plugin-legacy -s

vite.config.ts中引入并使用它。

考虑了 ie >= 11 版本的兼容。目前项目考虑的大部分场景都是 chrome 版本大于 100 的,就可以不需要考虑 ie 了。直接置为默认配置就好

ts 复制代码
    import legacyPlugin from "@vitejs/plugin-legacy";

    export default defineConfig({
      // ...
      plugins: [
        legacyPlugin({
          targets: ["defaults", "not IE 11"],
        }),
      ],
    });
bash 复制代码
    dist/assets/polyfills-legacy.749f4000.js 68.82 KiB / gzip: 27.57 KiB
    dist/assets/index-legacy.a57b6cea.js 758.60 KiB / gzip: 235.17 KiB
    Done in 25.22s.

问题:打包时遇到警告 ⚠️

输出文件名字/static/vendor.9b5698e4.js 8806.03kb / brotli: skipped (large chunk)

WARN 18:49:04

(!) Some chunks are larger than 2000 KiB after minification. Consider:

  • Using dynamic import() to code-split the application
  • Use build.rollupOptions.output.manualChunks to improve chunking: rollupjs.org/guide/en/#o...
  • Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.

由于打包时有些依赖包体积过于庞大,提示你进行配置分割;

1、css 分开打包

首先对 css 进行拆包

ts 复制代码
    manualChunks(id) {
      if (id.includes(".css")) {
        if (id.includes("iconfont")) return "iconfont";
        if (id.includes("public")) return "public";
        if (id.includes("normalize")) return "normalize";
      }
    }

css 根据初始化样式(normalize)、字体图标样式(iconfont)、公共原子 css 方法库(public)等拆包

2、使用lodash-es代替lodash,减少构建后的代码产物大小

没啥特别的,全局将 lodash 替换成 lodash-es 就行

ts 复制代码
    import { cloneDeep } from "lodash-es";

3、第三方依赖(vendor)拆解分包

  • 核心模块打包,从 vendor 中拆分出来
  • 业务依赖拆包
  • 剩余模块分包,兜底走合并打包

1.核心模块打包,从 vendor 中拆分出来

ts 复制代码
    /**
    * vendor 拆解分包
    */
    const SPLIT_VENDOR_CONFIG = ['axios', 'vue-router', 'pinia', 'vue'];

    manualChunks(id) {
      if (id?.includes("node_modules")) {
        let chunk = "";
        // 核心模块优先单独打包
        SPLIT_VENDOR_CONFIG.forEach((item) => {
          if (id.includes(item)) {
            chunk = `${item}`;
          }
        });
        if (chunk !== "") return chunk;
      }
    }

2.业务依赖拆包

ts 复制代码
    /**
     * chunk 分隔策略
     * @param mapping 记录类型,包含键和值的字符串对
     * @param id id - 需要查询的目标ID字符串
     * @returns
     */
    const mapManualChunks = (mapping: Record<string, string>, id: string): string | undefined => {
      // 使用for...of循环遍历映射对象的每一对键值对
      for (const [match, chunk] of Object.entries(mapping)) {
        // 检查当前ID是否包含匹配字符串(match)
        if (id.includes(match)) {
          // 如果找到匹配项,立即返回对应的块(chunk)
          return chunk;
        }
      }
      // 如果没有找到匹配项,则返回undefined
      return void 0;
    };

    // 拆包
    const chunkName = mapManualChunks({
      'echarts': 'echarts',
      'jsplumb': 'jsplumb',
      'lodash-es': 'lodash-es',
      'element-plus': 'element-plus',
      'monaco-editor': 'monaco-editor'
    }, id);
    if (chunkName) return chunkName;

    manualChunks(id) {
      if (id?.includes("node_modules")) {
        // 拆包
        const chunkName = mapManualChunks({
          'echarts': 'echarts',
          'jsplumb': 'jsplumb',
          'lodash-es': 'lodash-es',
          'element-plus': 'element-plus',
          'monaco-editor': 'monaco-editor',
          'axios': 'axios',
          'vue-router': 'vue-router',
          'pinia': 'pinia',
          'vue': 'vue',
        }, id);
        if (chunkName) return chunkName;
      }
    }

3.剩余模块分包,兜底走合并打包

ts 复制代码
    manualChunks(id) {
      if (id?.includes("node_modules")) {
        const depName = id.toString()?.split("node_modules/")[1]?.split?.("/")?.[0];
        if (depName) {
          return depName.toString();
        }
        return "vendor";
      }
    }

最后贴下所有的分包策略配置:

ts 复制代码
    /**
     * 手动分包策略,拆解 vendor 包
     * @param id
     * @returns
     */
    function manualChunks(id: string) {
      if (id.includes(".css")) {
        if (id.includes("iconfont")) return "iconfont";
        if (id.includes("public")) return "public";
      }
      if (id.includes("node_modules")) {
        // 1.核心模块打包,从 vendor 中拆分出来
        let chunk = "";
        SPLIT_VENDOR_CONFIG.forEach((item) => {
          if (id.includes(item)) {
            chunk = `${item}`;
          }
        });
        if (chunk !== "") return chunk;

        // 2.业务依赖拆包
        const chunkName = mapManualChunks(
          {
            // 'vue': 'vue',
            echarts: "echarts",
            jsplumb: "jsplumb",
            "lodash-es": "lodash-es",
            "element-plus": "element-plus",
            "monaco-editor": "monaco-editor",
            "@tencent/delta-ui": "tencent-delta-ui",
            // 下面这些次要依赖,走旁路cdn的形式
            // '@antv/g2plot': 'g2plot',
            // '@antv/g6': 'g6',
            // '@antv/s2': 's2',
          },
          id
        );
        if (chunkName) return chunkName;

        // 3.剩余模块分包,兜底走合并打包
        const depName = id.toString()?.split("node_modules/")[1]?.split?.("/")?.[0];
        if (depName) {
          return depName.toString();
        }
        return "vendor";
      }
    }

五、依赖外部化(external)不打包,走旁路 cdn 的形式

可以看出@antv生态相关的库(@antv/g2plot@antv/g6@antv/s2)打包后体积挺大,并且只有在特定的业务场景下才需要用到,因此可以在打包的时候把需要外部化(external)的第三方库剥离出去,减少主包大小,提高首屏加载速度

1、依赖外部化(external)不打包

ts 复制代码
    const rollupOptions = {
      // 告诉rollup,不打包@antv/g2plot,@antv/g6,@antv/s2;将其视为外部依赖
      external: ["@antv/g2plot", "@antv/g6", "@antv/s2"],
      plugins: [
        // 设置全局变量
        externalGlobals({
          "@antv/g2plot": "G2Plot", // 告诉rollup, 全局变量G2Plot即是@antv/g2plot
          "@antv/g6": "G6",
          "@antv/s2": "S2",
        }),
      ],
    };

2、走旁路 cdn 的形式

2.1 手动引入 cdn 资源

我们需要在项目入口模版页面,手动引入依赖资源:

html 复制代码
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1.0" />
        <title><%- title %></title>
        <!-- 引入 G2Plot 的 CDN 链接,当 CDN 加载失败时, 使用备用 URL 进行加载 -->
        <script src="<%= injectG2plotScriptURL %>" crossorigin="anonymous"></script>

        <!-- 引入 G6 的 CDN 链接 -->
        <script src="<%= injectG6Script %>" crossorigin="anonymous"></script>

        <!-- 引入 S2 的 CDN 链接 -->
        <script src="<%= injectS2Script %>" crossorigin="anonymous"></script>
      </head>
    </html>

注意这里的资源地址,是配置在基于 vite-plugin-html 的 vite 插件,下面直接贴下配置:

关于 vite-plugin-html 这个插件相关的知识,小伙伴可以看这篇文章

js 复制代码
    import { type PluginOption } from "vite";
    import { createHtmlPlugin } from "vite-plugin-html";

    /**
     * 配置 html 插件(在 index.html 中最小化和使用 ejs 模板语法的插件。)
     * https://github.com/anncwb/vite-plugin-html
     * @param env 环境配置
     * @param isBuild 是否为生产环境
     */
    export function configHtmlPlugin(env: ViteEnv, isBuild = true) {
      const appTitle = "hello world";
      // 主要的CDN资源地址
      const MAIN_CDN_URL = "https://unpkg.com";
      const injectG2plotScriptURL = `${MAIN_CDN_URL}/@antv/g2plot@2.4.20/dist/g2plot.min.js`;
      const injectG6Script = `${MAIN_CDN_URL}/@antv/g6@4.6.8/dist/g6.min.js`;
      const injectS2Script = `${MAIN_CDN_URL}/@antv/s2@1.15.0/dist/index.min.js`;
      const htmlPlugin: PluginOption[] = createHtmlPlugin({
        minify: isBuild,
        template: "./index.html",
        inject: {
          // 将数据注入 ejs 模板
          data: {
            title: appTitle,
            injectScript: `<script type="module" src="./src/main.ts"></script>`,
            injectG2plotScriptURL,
            injectG6Script,
            injectS2Script,
          },
          // 嵌入生成的 app.config.js 文件
          tags: [],
        },
      });
      return htmlPlugin;
    }

2.2 cdn 资源支持容灾

当主 cdn 资源加载失败时,我们需要有备用 cdn 来加载资源,实现 cdn 容灾

模板文件增加资源加载错误处理:

当主 CDN 资源加载失败时,handleCDNError 函数会被调用,就尝试从备用 CDN(fallbackCDN)加载资源

html 复制代码
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <title><%- title %></title>
    <script>
      function handleCDNError(fallbackCDNUrl) {
        if (fallbackCDNUrl) {
          const script = document.createElement("script");
          script.src = fallbackCDNUrl;
          document.head.appendChild(script);
        }
      }
    </script>
    <!-- 引入 G2Plot 的 CDN 链接,当 CDN 加载失败时, 使用备用 URL 进行加载 -->
    <script
      src="<%= injectG2plotScriptURL %>"
      onerror="handleCDNError('<%= injectG2plotScriptFallbackURL %>')"
      crossorigin="anonymous"
    ></script>

    <!-- 引入 G6 的 CDN 链接 -->
    <script
      src="<%= injectG6Script %>"
      onerror="handleCDNError('<%= injectG6ScriptFallbackURL %>')"
      crossorigin="anonymous"
    ></script>

    <!-- 引入 S2 的 CDN 链接 -->
    <script
      src="<%= injectS2Script %>"
      onerror="handleCDNError('<%= injectS2FallbackURL %>')"
      crossorigin="anonymous"
    ></script>
  </head>
</html>
vite-plugin-html 插件增加备用 CDN 地址
js 复制代码
import { type PluginOption } from "vite";
import { createHtmlPlugin } from "vite-plugin-html";

/**
 * 配置 html 插件(在 index.html 中最小化和使用 ejs 模板语法的插件。)
 * https://github.com/anncwb/vite-plugin-html
 * @param env 环境配置
 * @param isBuild 是否为生产环境
 */
export function configHtmlPlugin(env: ViteEnv, isBuild = true) {
  const appTitle = "hello world";
  // 主要的CDN资源地址
  const MAIN_CDN_URL = "https://unpkg.com";
  // 备用的CDN资源地址
  // 当主 CDN 资源加载失败时,handleCDNError 函数会被调用,就尝试从备用 CDN(fallbackCDN)加载资源
  const FALLBACK_CDN_URL = "https://cdn.jsdelivr.net/npm";
  const injectG2plotScriptURL = `${MAIN_CDN_URL}/@antv/g2plot@2.4.20/dist/g2plot.min.js`;
  const injectG2plotScriptFallbackURL = `${FALLBACK_CDN_URL}/@antv/g2plot@2.4.20/dist/g2plot.min.js`;
  const injectG6Script = `${MAIN_CDN_URL}/@antv/g6@4.6.8/dist/g6.min.js`;
  const injectG6ScriptFallbackURL = `${FALLBACK_CDN_URL}/@antv/g6@4.6.8/dist/g6.min.js`;
  const injectS2Script = `${MAIN_CDN_URL}/@antv/s2@1.15.0/dist/index.min.js`;
  const injectS2FallbackURL = `${FALLBACK_CDN_URL}/@antv/s2@1.15.0/dist/index.min.js`;
  const htmlPlugin: PluginOption[] = createHtmlPlugin({
    minify: isBuild,
    template: "./index.html",
    inject: {
      // 将数据注入 ejs 模板
      data: {
        title: appTitle,
        injectScript: `<script type="module" src="./src/main.ts"></script>`,
        injectG2plotScriptURL,
        injectG2plotScriptFallbackURL,
        injectG6Script,
        injectG6ScriptFallbackURL,
        injectS2Script,
        injectS2FallbackURL,
        // `,
      },
      // 嵌入生成的 app.config.js 文件
      tags: [],
    },
  });
  return htmlPlugin;
}

这样,当主 CDN 资源加载失败时,就可以自动从备用 CDN(fallbackCDN)加载资源

六、图片压缩优化

虽然整站的图片资源还达不到性能瓶颈,但整体的图片量还是占据不小空间的:

这里使用的是 vite-plugin-imagemingithub.com/vbenjs/vite...

1、安装vite-plugin-imagemin插件,对项目中的图片进行压缩处理。

bash 复制代码
    npm i vite-plugin-imagemin -D

2、在vite.config.ts中引入并使用它。

js 复制代码
    import viteImagemin from "vite-plugin-imagemin";

    viteImagemin({
      gifsicle: {
        optimizationLevel: 7,
        interlaced: false,
      },
      optipng: {
        optimizationLevel: 7,
      },
      mozjpeg: {
        quality: 20,
      },
      pngquant: {
        quality: [0.8, 0.9],
        speed: 4,
      },
      svgo: {
        plugins: [
          {
            name: "removeViewBox",
          },
          {
            name: "removeEmptyAttrs",
            active: false,
          },
        ],
      },
    });

重新打包看看效果:

matlab 复制代码
✨ [vite-plugin-imagemin]- compressed image resource successfully:
dist/assets/static/down_active-5d23b9dd.svg -4% 0.89kb / tiny: 0.86kb 17:45:34
dist/assets/static/delete_active-56cfe149.svg -7% 0.43kb / tiny: 0.40kb 17:45:34
dist/assets/static/manage_active-879d73a1.svg -1% 4.21kb / tiny: 4.18kb 17:45:34
dist/assets/static/dic-arrow-up-de8e50f4.svg -5% 0.54kb / tiny: 0.52kb 17:45:34
dist/assets/static/dic-arrow-down-6028b3ec.svg -5% 0.54kb / tiny: 0.52kb 17:45:34
dist/assets/static/Group-2fc9476e.svg -3% 0.84kb / tiny: 0.82kb 17:45:34
dist/assets/static/dict_star-5f885ed8.svg -5% 0.60kb / tiny: 0.58kb 17:45:34
dist/assets/static/dict_star_active-54868280.svg -5% 0.60kb / tiny: 0.58kb 17:45:34
dist/assets/static/search-container-8db4149a.svg -5% 0.61kb / tiny: 0.58kb 17:45:34
dist/assets/static/input-add-btn-6e5cc09a.svg -7% 0.42kb / tiny: 0.39kb 17:45:34
dist/assets/static/search-container-active-bf2e6aa4.svg -5% 0.61kb / tiny: 0.58kb 17:45:34
dist/assets/static/input-add-btn-active-25b61836.svg -7% 0.42kb / tiny: 0.39kb 17:45:34
dist/assets/static/close-active-ce2971f8.svg -4% 0.67kb / tiny: 0.64kb 17:45:34
dist/assets/static/close-dd4bf350.svg -4% 0.67kb / tiny: 0.64kb 17:45:34
dist/assets/static/add-6938e6ea.svg -4% 0.65kb / tiny: 0.62kb 17:45:34
dist/assets/static/off-computer-c1b929a5.svg -3% 1.27kb / tiny: 1.24kb 17:45:34
dist/assets/static/offcp-computer-54c10076.svg -3% 1.27kb / tiny: 1.24kb 17:45:34
dist/assets/static/off-sql-script-82b70d31.svg -2% 2.04kb / tiny: 2.01kb 17:45:34
dist/assets/static/unfold-5adf4cbf.svg -4% 0.72kb / tiny: 0.69kb 17:45:34
dist/assets/static/offcp-sql-script-3d42873b.svg -2% 2.04kb / tiny: 2.01kb 17:45:34
dist/assets/static/real-jar-e6b4e97a.svg -2% 1.89kb / tiny: 1.87kb 17:45:34
dist/assets/static/real-indicator-0284e71d.svg -2% 1.41kb / tiny: 1.39kb 17:45:34
dist/assets/static/real-sql-script-333be252.svg -2% 2.04kb / tiny: 2.01kb 17:45:34
dist/assets/static/realcp-indicator-a8fd774a.svg -2% 1.41kb / tiny: 1.39kb 17:45:34
dist/assets/static/real-timecompute-8617b761.svg -3% 1.27kb / tiny: 1.24kb 17:45:34
dist/assets/static/realcp-jar-e0457e9b.svg -2% 1.89kb / tiny: 1.87kb 17:45:34
dist/assets/static/message-a1b11bf6.svg -3% 0.88kb / tiny: 0.86kb 17:45:34
dist/assets/static/big-data-2a162ca1.svg -3% 1.11kb / tiny: 1.08kb 17:45:34
dist/assets/static/checked-exploit-956e4893.svg -3% 1.12kb / tiny: 1.09kb 17:45:34
dist/assets/static/checked-machine-263d6a3e.svg -2% 1.47kb / tiny: 1.44kb 17:45:34
dist/assets/static/machine-learning-c1e44256.svg -2% 1.47kb / tiny: 1.44kb 17:45:34
dist/assets/static/question-0c64b1f1.svg -1% 9.18kb / tiny: 9.15kb 17:45:34
dist/assets/static/right-f1c797d4.svg -5% 0.58kb / tiny: 0.56kb 17:45:34
dist/assets/static/right_active-c2e9219e.svg -5% 0.58kb / tiny: 0.56kb 17:45:34
dist/assets/static/permissions-approved-731bfe4a.svg -1% 2.80kb / tiny: 2.77kb 17:45:34
dist/assets/static/permissions-approving-6b08fa8b.svg -2% 1.88kb / tiny: 1.85kb 17:45:34
dist/assets/static/permissions-invalid-dd9d1cc2.svg -1% 2.48kb / tiny: 2.46kb 17:45:34
dist/assets/static/permissions-rejected-b3875e01.svg -2% 2.04kb / tiny: 2.02kb 17:45:34
dist/assets/static/permissions-revoked-99ba7396.svg -1% 3.21kb / tiny: 3.18kb 17:45:34
dist/assets/static/permissions-timeout-19dabe3a.svg -2% 2.12kb / tiny: 2.10kb 17:45:34
dist/assets/static/info-340e6de3.svg +3% 1.14kb / tiny: 1.18kb 17:45:34
dist/assets/static/canvas_search-8f72cecf.svg -4% 0.60kb / tiny: 0.57kb 17:45:34
dist/assets/static/dict_public-dd9c9f95.svg -2% 1.69kb / tiny: 1.66kb 17:45:34
dist/assets/static/dict_secrecy-0585b6cd.svg -1% 2.86kb / tiny: 2.84kb 17:45:34
dist/assets/static/dict_secret-74c89440.svg -1% 3.55kb / tiny: 3.52kb 17:45:34
dist/assets/static/dict_tosecret-9146d1c8.svg -1% 3.11kb / tiny: 3.08kb 17:45:34
dist/assets/static/target_empty-b867e578.svg -4% 0.71kb / tiny: 0.69kb 17:45:34
dist/assets/static/info_hint-5bd85a79.svg -2% 1.43kb / tiny: 1.41kb 17:45:34
dist/assets/static/refresh-85ed4e44.svg -2% 1.56kb / tiny: 1.54kb 17:45:34
dist/assets/static/catalogue-48de0b73.svg -3% 1.24kb / tiny: 1.20kb 17:45:34
dist/assets/static/sql_active-b0077155.svg -13% 0.93kb / tiny: 0.82kb 17:45:34
dist/assets/static/sql-ac7708c0.svg -13% 0.86kb / tiny: 0.75kb 17:45:34
dist/assets/static/catalogue_active-f09f5c4b.svg -3% 1.25kb / tiny: 1.21kb 17:45:34
dist/assets/static/function_active-1f1cd518.svg -8% 0.60kb / tiny: 0.55kb 17:45:34
dist/assets/static/function-54d022cb.svg -8% 0.53kb / tiny: 0.49kb 17:45:34
dist/assets/static/index-tab-active-51fcc081.svg -7% 0.65kb / tiny: 0.61kb 17:45:34
dist/assets/static/index-tab-82a8a4ea.svg -7% 0.64kb / tiny: 0.60kb 17:45:34
dist/assets/static/index-alone-f4614032.svg -7% 0.58kb / tiny: 0.54kb 17:45:34
dist/assets/static/index-alone-active-6becc497.svg -7% 0.57kb / tiny: 0.54kb 17:45:34
dist/assets/static/struct-03b8b891.svg -5% 0.58kb / tiny: 0.56kb 17:45:34
dist/assets/static/struct_active-e9b7de78.svg -5% 0.60kb / tiny: 0.58kb 17:45:34
dist/assets/static/link-aeb97272.svg -6% 0.47kb / tiny: 0.44kb 17:45:34
dist/assets/static/link_active-266dab44.svg -6% 0.47kb / tiny: 0.44kb 17:45:34
dist/assets/static/dic-arrow-2816defd.svg -5% 0.54kb / tiny: 0.52kb 17:45:34
dist/assets/static/personal-8d2a7a6d.svg -7% 0.42kb / tiny: 0.39kb 17:45:34
dist/assets/static/space-463ef793.svg -5% 0.65kb / tiny: 0.62kb 17:45:34
dist/assets/static/test-result-empty-f7de3cb8.svg -4% 0.73kb / tiny: 0.71kb 17:45:34
dist/assets/static/current_space-45177d98.png -74% 4.83kb / tiny: 1.27kb 17:45:34
dist/assets/static/space_state-80fdf99b.png -82% 14.38kb / tiny: 2.59kb 17:45:34
dist/assets/static/space_img-b0a421a4.png -76% 14.55kb / tiny: 3.55kb 17:45:34
dist/assets/static/add-image-41a92e18.png -66% 63.97kb / tiny: 22.37kb
dist/assets/static/ce-ee6bcf54.png -79% 25.52kb / tiny: 5.49kb 17:45:34
dist/assets/static/user-img-cb998d2e.png -80% 72.35kb / tiny: 15.19kb
...
dist/assets/static/banner-0851e331.png -10% 182.15kb / tiny: 164.44kb
17:45:34

✨ Done in 195.62s.

这样就解决了图片资源过大的问题,图片资源后续还可以考虑上传到 CDN 加速。

这里会存在一个问题:每次打包都会走一次图片压缩。

虽然好处是不会对原图片产生影响,但是会导致重复劳动,占用打包时间。既然生产是需要用压缩后的图片了,那么对源图片就没有必要留住了。所以参考了vite-plugin-imageminimagemin 写了一个脚本,可以实现如下功能:

  1. 打包之前自动执行这个脚本进行图片压缩
  2. 压缩之后生成一个 imagemin.map.json 文件,是记录哪些图片已经压缩过了,不需要二次压缩
  3. 压缩之前检查当前图片的 修改时间 值是否在 imagemin.map.json 文件内,在的话则过滤走,不在才需要压缩
  4. 压缩之后的图片,覆盖原本路径的处理
  5. 有些图片在压缩之后反而比原图更大了,对这些图片不做覆盖原图处理,直接保留原图,但是需要在 imagemin.map.json记录该文件以及源文件的 修改时间

packages.json 添加前置脚本,执行 yarn build:test 的时候就会自动先执行 yarn prebuild:test

scripts/imagemin.mjs 的代码 👇🏻:

js 复制代码
    import { promises as fs } from 'fs';
    import path from 'node:path';
    import { fileURLToPath } from 'node:url';
    import { globby } from 'globby';
    import chalk from 'chalk';
    import convertToUnixPath from 'slash';
    import ora from 'ora';
    import imagemin from 'imagemin';
    import imageminGifsicle from 'imagemin-gifsicle';
    import imageminOptpng from 'imagemin-optipng';
    import imageminMozjpeg from 'imagemin-mozjpeg';
    import imageminPngquant from 'imagemin-pngquant';
    import imageminSvgo from 'imagemin-svgo';

    // 1. 建立imagemin.map.json缓存表,如果已经处理过,则不再处理,处理过就更新到imagemin.map.json
    // 2. 需要覆盖原图,assets/images下有多个文件夹,所以需要解决dest的路径问题,需要用imagemin.buffer来重写
    // 3. 有些图片在压缩完之后会变得更大,这种情况不覆盖写入文件,但是要写入缓存文件,且时间戳是旧文件自己的时间戳
    // 4. 更多图片类型的插件见 https://github.com/orgs/imagemin/repositories?type=all

    // 缓存文件
    let cacheFilename = './imagemin.map.json';
    // 图片文件目录
    const input = ['src/assets/img/**/*.{jpg,png,svg,gif}'];
    // 插件
    const plugins = [
      imageminGifsicle({
        optimizationLevel: 7,
        interlaced: false
      }),
      imageminOptpng({
        optimizationLevel: 7
      }),
      imageminMozjpeg({
        quality: 80
      }),
      imageminPngquant({
        quality: [0.8, 0.9],
        speed: 4
      }),
      imageminSvgo({
        plugins: [
          {
            name: 'removeViewBox'
          },
          {
            name: 'removeEmptyAttrs',
            active: false
          }
        ]
      })
    ];
    const debug = false;
    const tinyMap = new Map();
    let filePaths = [];
    let cache, cachePath;
    let handles = [];
    let time;
    const spinner = ora('图片压缩中...');
    (async () => {
      const unixFilePaths = input.map((path) => convertToUnixPath(path));
      cachePath = path.resolve(
        path.dirname(fileURLToPath(import.meta.url)),
        cacheFilename
      );
      cache = await fs.readFile(cachePath);
      cache = JSON.parse(cache.toString() || '{}');
      // 通过通配符匹配文件路径
      filePaths = await globby(unixFilePaths, { onlyFiles: true });
      // 如果文件不在imagemin.map.json上,则加入队列;
      // 如果文件在imagemin.map.json上,且修改时间不一致,则加入队列;
      filePaths = await filter(filePaths, async (filePath) => {
        let ctimeMs = cache[filePath];
        let mtimeMs = (await fs.stat(filePath)).mtimeMs;
        if (!ctimeMs) {
          debug &amp;amp;&amp;amp; console.log(filePath + '不在缓存入列');
          tinyMap.set(filePath, {
            mtimeMs
          });
          return true;
          // 系统时间戳,比Date.now()更精准,多了小数点后三位,所以控制在1ms内都认为是有效缓存
        } else {
          if (Math.abs(ctimeMs - mtimeMs) > 1) {
            debug &amp;amp;&amp;amp;
              console.log(`
              ${filePath}在缓存但过期了而入列,${ctimeMs} ${mtimeMs} 相差${
                ctimeMs - mtimeMs
              }`);
            tinyMap.set(filePath, {
              mtimeMs
            });
            return true;
          } else {
            // debug &amp;amp;&amp;amp; console.log(filePath + '在缓存而出列');
            return false;
          }
        }
      });
      debug &amp;amp;&amp;amp; console.log(filePaths);
      await processFiles();
    })();
    // 处理单个文件,调用imagemin.buffer处理
    async function processFile(filePath) {
      let buffer = await fs.readFile(filePath);
      let content;
      try {
        content = await imagemin.buffer(buffer, {
          plugins
        });

        const size = content.byteLength,
          oldSize = buffer.byteLength;

        if (tinyMap.get(filePath)) {
          tinyMap.set(filePath, {
            ...tinyMap.get(filePath),
            size: size / 1024,
            oldSize: oldSize / 1024,
            ratio: size / oldSize - 1
          });
        } else {
          tinyMap.set(filePath, {
            size: size / 1024,
            oldSize: oldSize / 1024,
            ratio: size / oldSize - 1
          });
        }

        return content;
      } catch (error) {
        console.error('imagemin error:' + filePath);
      }
    }
    // 批量处理
    async function processFiles() {
      if (!filePaths.length) {
        return;
      }
      spinner.start();
      time = Date.now();
      handles = filePaths.map(async (filePath) => {
        let content = await processFile(filePath);
        return {
          filePath,
          content
        };
      });
      handles = await Promise.all(handles);
      await generateFiles();
    }
    // 生成文件并覆盖源文件
    async function generateFiles() {
      if (handles.length) {
        handles = handles.map(async (item) => {
          const { filePath, content } = item;
          if (content) {
            if (tinyMap.get(filePath).ratio < 0) {
              await fs.writeFile(filePath, content);
              cache[filePath] = Date.now();
            } else {
              // 存在压缩之后反而变大的情况,这种情况不覆盖原图,但会记录到缓存表中,且记录的时间戳是旧文件自己的时间戳
              cache[filePath] = tinyMap.get(filePath).mtimeMs;
            }
          }
        });
        handles = await Promise.all(handles);
        handleOutputLogger();
        generateCache();
      }
    }
    // 生成缓存文件
    async function generateCache() {
      await fs.writeFile(cachePath, Buffer.from(JSON.stringify(cache)), {
        encoding: 'utf-8'
      });
    }
    // 输出结果
    function handleOutputLogger() {
      spinner.stop();
      console.info('图片压缩成功');
      time = (Date.now() - time) / 1000 + 's';
      const keyLengths = Array.from(tinyMap.keys(), (name) => name.length);
      const valueLengths = Array.from(
        tinyMap.values(),
        (value) => `${Math.floor(100 * value.ratio)}`.length
      );

      const maxKeyLength = Math.max(...keyLengths);
      const valueKeyLength = Math.max(...valueLengths);
      tinyMap.forEach((value, name) => {
        let { ratio } = value;
        const { size, oldSize } = value;
        ratio = Math.floor(100 * ratio);
        const fr = `${ratio}`;

        // 存在压缩之后反而变大的情况,这种情况不覆盖原图,所以这种情况显示0%
        const denseRatio =
          ratio > 0
            ? // ? chalk.red(`+${fr}%`)
              chalk.green(`0%`)
            : ratio <= 0
            ? chalk.green(`${fr}%`)
            : '';

        const sizeStr =
          ratio <= 0
            ? `${oldSize.toFixed(2)}kb / tiny: ${size.toFixed(2)}kb`
            : `${oldSize.toFixed(2)}kb / tiny: ${oldSize.toFixed(2)}kb`;

        console.info(
          chalk.dim(
            chalk.blueBright(name) +
              ' '.repeat(2 + maxKeyLength - name.length) +
              chalk.gray(
                `${denseRatio} ${' '.repeat(valueKeyLength - fr.length)}`
              ) +
              ' ' +
              chalk.dim(sizeStr)
          )
        );
      });
      console.info('图片压缩总耗时', time);
    }
    // filter不支持异步处理,用map来模拟filter
    // https://stackoverflow.com/questions/33355528/filtering-an-array-with-a-function-that-returns-a-promise/46842181#46842181
    async function filter(arr, callback) {
      const fail = Symbol();
      return (
        await Promise.all(
          arr.map(async (item) => ((await callback(item)) ? item : fail))
        )
      ).filter((i) => i !== fail);
    }

再次打包看看效果 👇🏻:

bash 复制代码
    yarn run v1.22.10
    $ node ./build/scripts/imagemin.mjs
    图片压缩成功
    src/assets/img/algh_svg/add.png -3% 0.27kb / tiny: 0.26kb
    src/assets/img/algh_svg/fail.png 0% 0.31kb / tiny: 0.31kb
    src/assets/img/algh_svg/file.png 0% 0.23kb / tiny: 0.23kb
    src/assets/img/algh_svg/loading.png -12% 0.59kb / tiny: 0.52kb
    src/assets/img/algh_svg/not.png -5% 0.31kb / tiny: 0.30kb
    src/assets/img/algh_svg/receive.png -8% 0.38kb / tiny: 0.35kb
    src/assets/img/algh_svg/open.png -5% 0.37kb / tiny: 0.35kb
    src/assets/img/algh_svg/success.png -4% 0.32kb / tiny: 0.31kb
    src/assets/img/bdpManage/progress-fail.png 0% 0.36kb / tiny: 0.36kb
    src/assets/img/bdpManage/progress-revoked.png 0% 0.36kb / tiny: 0.36kb
    src/assets/img/bdpManage/progress-load.png -4% 0.28kb / tiny: 0.27kb
    src/assets/img/bdpManage/progress-success.png -5% 0.33kb / tiny: 0.31kb
    src/assets/img/bdpManage/progress-timeout.png -2% 0.27kb / tiny: 0.26kb
    src/assets/img/common-share-canvas-layout/loading.png -12% 0.59kb / tiny: 0.52kb
    src/assets/img/env-detail/close.png -3% 0.41kb / tiny: 0.40kb
    src/assets/img/env-detail/fail.png 0% 0.31kb / tiny: 0.31kb
    src/assets/img/env-detail/running.png -11% 0.59kb / tiny: 0.53kb
    src/assets/img/env-detail/success.png 0% 0.31kb / tiny: 0.31kb
    src/assets/img/header/ce.png -6% 4.92kb / tiny: 4.66kb
    src/assets/img/header/wiki.png -8% 0.92kb / tiny: 0.85kb
    src/assets/img/explore/banner.png -6% 161.91kb / tiny: 152.25kb
    src/assets/img/explore/bg_footer.png 0% 0.57kb / tiny: 0.57kb
    src/assets/img/explore/bg_footer2.png 0% 0.57kb / tiny: 0.57kb
    src/assets/img/explore/bg_footer_sw.png 0% 0.57kb / tiny: 0.57kb
    src/assets/img/explore/bg_footer2_sw.png 0% 0.57kb / tiny: 0.57kb
    src/assets/img/explore/copy_sql.png -5% 0.37kb / tiny: 0.35kb
    src/assets/img/explore/copy_sql_rev.png -2% 0.39kb / tiny: 0.39kb
    src/assets/img/explore/copy_text.png -8% 0.38kb / tiny: 0.35kb
    src/assets/img/explore/copy_text_rev.png -5% 0.41kb / tiny: 0.39kb
    src/assets/img/explore/drag-icon.png -6% 0.38kb / tiny: 0.36kb
    src/assets/img/explore/drag.png -1% 0.36kb / tiny: 0.36kb
    src/assets/img/explore/edit_rev.png -2% 0.34kb / tiny: 0.33kb
    src/assets/img/explore/edit.png -2% 0.31kb / tiny: 0.31kb
    src/assets/img/explore/current_space.png -13% 1.25kb / tiny: 1.10kb
    src/assets/img/explore/fail.png 0% 0.32kb / tiny: 0.32kb
    src/assets/img/explore/file.png 0% 0.24kb / tiny: 0.24kb
    src/assets/img/explore/folder.png -4% 0.35kb / tiny: 0.34kb
    src/assets/img/explore/full_screen.png -4% 0.41kb / tiny: 0.40kb
    src/assets/img/explore/no_auth.png -3% 0.29kb / tiny: 0.28kb
    src/assets/img/explore/no_auth_rev.png -2% 0.30kb / tiny: 0.30kb
    src/assets/img/explore/readonly.png -4% 0.30kb / tiny: 0.29kb
    src/assets/img/explore/readonly_rev.png -7% 0.44kb / tiny: 0.41kb
    src/assets/img/explore/running.png -4% 0.34kb / tiny: 0.33kb
    src/assets/img/explore/space_img.png -3% 3.15kb / tiny: 3.05kb
    src/assets/img/explore/space_state.png 0% 2.56kb / tiny: 2.56kb
    src/assets/img/explore/table_rev.png -4% 0.22kb / tiny: 0.22kb
    src/assets/img/explore/success.png -4% 0.26kb / tiny: 0.25kb
    src/assets/img/explore/table.png -6% 0.23kb / tiny: 0.22kb
    src/assets/img/explore/upload.png -2% 0.38kb / tiny: 0.38kb
    src/assets/img/explore/text.png -3% 0.22kb / tiny: 0.22kb
    src/assets/img/market/dic-close.png -9% 0.30kb / tiny: 0.28kb
    src/assets/img/market/info.png -4% 0.35kb / tiny: 0.34kb
    src/assets/img/market/tooltip1.png -3% 23.67kb / tiny: 23.18kb
    src/assets/img/server_deploy/add_row.png 0% 0.24kb / tiny: 0.24kb
    src/assets/img/service/add-image.png -9% 20.66kb / tiny: 18.87kb
    src/assets/img/service/close.png 0% 0.51kb / tiny: 0.51kb
    src/assets/img/service/hr.png -15% 0.23kb / tiny: 0.20kb
    src/assets/img/service/info.png 0% 0.39kb / tiny: 0.39kb
    src/assets/img/service/model-image.png -1% 0.64kb / tiny: 0.63kb
    src/assets/img/service/rightTip.png 0% 0.47kb / tiny: 0.47kb
    src/assets/img/service/rush.png -5% 0.35kb / tiny: 0.33kb
    src/assets/img/service/success.png -3% 0.57kb / tiny: 0.55kb
    图片压缩总耗时 21.04s

    ...

    dist/assets/static/test-result-empty-8ca47376.svg 0% 0.71kb / tiny: 0.71kb 19:28:07
    dist/assets/static/add-image-14b1f8f4.png -8% 18.87kb / tiny: 17.42kb 19:28:07
    dist/assets/static/ce-5914e36b.png -2% 4.66kb / tiny: 4.59kb 19:28:07
    dist/assets/static/user-img-fab896be.png +1% 15.23kb / tiny: 15.43kb 19:28:07
    dist/assets/static/banner-de979ef1.png -13% 152.25kb / tiny: 132.53kb 19:28:07
    19:28:07

    ✨ Done in 246.62s.

可以看到,第一次打包的时候需要额外耗时 195.62s,第二次打包在不添加图片的情况下平均耗时 246.62 - 195.62 = 51s。如果后面添加了新图片,也只对新图片进行压缩。

这种方案也有缺点:

  1. 覆盖原图,在设置压缩插件参数的时候,不方便看效果,需要先备份好
  2. 对指定目录下匹配通配符后缀的图片文件都会进行压缩,而不是业务组件引用了哪些图片,哪些图片才需要压缩

关于 imagemin 的备注:

  1. imagemin 这个包需要在 node >= 12.20.0 执行;在国内下载会比较慢,需要在 package.json 加字段 "resolutions": { "bin-wrapper": "npm:bin-wrapper-china" }
  2. imagemin 以及其他几个包都是 ESM 格式的包,没有做 commonjs 规范的兼容,所以在 node 环境执行我写的脚本,最好把脚本的后缀改为 .mjs ,另外一种方案是在 package.json 加字段 { "type": "module" },但考虑到其他脚本需要使用commonjs 规范,就没这么做了

优化后:

七、pnpm 构建加速

使用 yarn 构建耗时246.62s

bash 复制代码
    ...
    dist/assets/static/space-d065198d.svg 0% 0.62kb / tiny: 0.62kb 19:28:07
    dist/assets/static/test-result-empty-8ca47376.svg 0% 0.71kb / tiny: 0.71kb 19:28:07
    dist/assets/static/add-image-14b1f8f4.png -8% 18.87kb / tiny: 17.42kb 19:28:07
    dist/assets/static/ce-5914e36b.png -2% 4.66kb / tiny: 4.59kb 19:28:07
    dist/assets/static/user-img-fab896be.png +1% 15.23kb / tiny: 15.43kb 19:28:07
    dist/assets/static/banner-de979ef1.png -13% 152.25kb / tiny: 132.53kb 19:28:07
    19:28:07

    ✨ Done in 246.62s.

使用 安装依赖 &amp; 图片压缩 &amp; 构建编译总耗时199.52s

bash 复制代码
    pnpm install
    pnpm run build
bash 复制代码
    src/assets/img/service/dic-arrow.svg 0% 0.52kb / tiny: 0.52kb
    src/assets/img/service/hr.png 0% 0.20kb / tiny: 0.20kb
    src/assets/img/service/info.png 0% 0.39kb / tiny: 0.39kb
    src/assets/img/service/message.svg 0% 0.86kb / tiny: 0.86kb
    src/assets/img/service/model-image.png -7% 0.63kb / tiny: 0.59kb
    src/assets/img/service/rightTip.png 0% 0.47kb / tiny: 0.47kb
    src/assets/img/service/rush.png -4% 0.33kb / tiny: 0.32kb
    src/assets/img/service/success.png -3% 0.55kb / tiny: 0.53kb
    src/assets/img/service/user-img.png 0% 15.23kb / tiny: 15.23kb
    src/assets/img/application/business-course/empty-task.svg 0% 3.08kb / tiny: 3.08kb
    src/assets/img/application/business-course/indicator-node-icon.svg 0% 1.38kb / tiny: 1.38kb
    src/assets/img/application/business-course/jar-node-icon.svg 0% 2.33kb / tiny: 2.33kb
    src/assets/img/application/business-course/simple-node-icon.svg 0% 1.38kb / tiny: 1.38kb
    src/assets/img/application/business-course/sql-node-icon.svg 0% 2.62kb / tiny: 2.62kb
    src/assets/img/application/business-course/wormhole-node-icon.svg 0% 0.56kb / tiny: 0.56kb
    src/assets/img/application/real-time-indicator-task/test-result-empty.svg 0% 0.71kb / tiny: 0.71kb
    src/assets/img/application/sql-task-instance/filter.svg 0% 1.61kb / tiny: 1.61kb
    src/assets/img/application/sql-task-instance/refresh.svg 0% 1.54kb / tiny: 1.54kb
    src/assets/img/component/light-context-container/arrow.svg 0% 0.69kb / tiny: 0.69kb
    图片压缩总耗时 33.361s

    ...

    dist/assets/static/info_hint-6d4c6bf7.svg 0% 0.82kb / tiny: 0.82kb 11:31:52
    dist/assets/static/alert-e7b4adaf.svg 0% 0.69kb / tiny: 0.69kb 11:31:52
    dist/assets/static/empty-task-b1e391d3.svg 0% 3.08kb / tiny: 3.08kb 11:31:52
    dist/assets/static/indicator-node-icon-a889562e.svg 0% 1.38kb / tiny: 1.38kb 11:31:52
    dist/assets/static/jar-node-icon-08d6e687.svg 0% 2.33kb / tiny: 2.33kb 11:31:52
    dist/assets/static/simple-node-icon-d6a6749c.svg 0% 1.38kb / tiny: 1.38kb 11:31:52
    dist/assets/static/sql-node-icon-1c01094a.svg 0% 2.62kb / tiny: 2.62kb 11:31:52
    dist/assets/static/wormhole-node-icon-a378d7cc.svg 0% 0.56kb / tiny: 0.56kb 11:31:52
    dist/assets/static/canvas_message-56d1f745.svg 0% 0.82kb / tiny: 0.82kb 11:31:52
    dist/assets/static/owner-2bae63fd.svg +3% 1.36kb / tiny: 1.41kb 11:31:52
    dist/assets/static/personal-b4c08ef5.svg 0% 0.39kb / tiny: 0.39kb 11:31:52
    dist/assets/static/space-d065198d.svg 0% 0.62kb / tiny: 0.62kb 11:31:52
    dist/assets/static/checked-help-4f2caf38.svg 0% 1.00kb / tiny: 1.00kb 11:31:52
    dist/assets/static/closed-f8b5e01f.svg 0% 0.79kb / tiny: 0.79kb 11:31:52
    dist/assets/static/cors-55615d37.svg 0% 1.21kb / tiny: 1.21kb 11:31:52
    dist/assets/static/help-7fc8fa43.svg 0% 1.00kb / tiny: 1.00kb 11:31:52
    dist/assets/static/test-result-empty-8ca47376.svg 0% 0.71kb / tiny: 0.71kb 11:31:52
    dist/assets/static/add-image-14b1f8f4.png -8% 18.87kb / tiny: 17.42kb 11:31:52
    dist/assets/static/ce-5914e36b.png -2% 4.66kb / tiny: 4.59kb 11:31:52
    dist/assets/static/user-img-fab896be.png +1% 15.23kb / tiny: 15.43kb 11:31:52
    dist/assets/static/banner-de979ef1.png -13% 152.25kb / tiny: 132.53kb 11:31:52
    11:31:52

    ✨ Done in 199.52s.

这里主要是多了图片压缩的耗时 33.361s ,相当于纯安装依赖 &amp; 构建时间耗时约为 166s

总结

通过打包分析,确认了图片压缩导致了构建时间长(79.07 s),确认了以下因素导致了主包过大(共 568.19 kiB(gziped) , vendor 530.72 KiB(gziped)index 37.47 KiB(gziped) ):

  1. lodash &amp; lodash-es全量打包
  2. css 全量打包到一块
  3. legacy 包存在冗余
  4. @antv/g2plot@antv/g6 大包
  5. vendor 巨大
  6. 图片资源多

解决方案:

  1. 通过 manualChunks 方案把 lodash &amp; lodash-es 独立打包并按需打包,减少主包体积
  2. css 根据公共样式、字体图标样式、原子 css 方法库等拆包
  3. 通过去掉冗余配置,减轻 legacy
  4. @antv/g2plot@antv/g6不打包,而是作为外部依赖external出去
  5. 调整依赖包打包策略:

(1)优先按照模块分开打包,大模块独立打包

(2)小模块按照兜底策略走合成打包

  1. 通过脚本,在构建之前压缩图片,并通过缓存避免下次重复压缩,优化构建过程

八、成果:

1、主包资源大小优化

主包从 14539.05 / gzip: 3775.69 KiB 减少到 4312.29 KiB / gzip: 1119.87KiB,体积减少了 70.4%

优化前(资源包大小):

bash 复制代码
    dist/assets/index.a19385eb.js 123.69 KiB / gzip: 35.44 KiB
    dist/assets/cssMode.9ee61238.js 243.01 KiB / gzip: 56.09 KiB
    dist/assets/index.d726b0d8.js 516.56 KiB / gzip: 151.15 KiB
    dist/assets/index.66a838f5.css 1177.79 KiB / gzip: 166.47 KiB
    dist/assets/index.9275298d.js 14539.05 KiB / gzip: 3775.69 KiB

    (!) Some chunks are larger than 2000 KiB after minification. Consider:
    - Using dynamic import() to code-split the application
    - Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/guide/en/#outputmanualchunks
    - Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.

    ✨ Done in 246.62s.

优化后(资源包大小):

bash 复制代码
    dist/assets/js/useDraftIntercept-0503743f.js 0.75 KiB 21:34:40
    dist/assets/js/index-5d6905dd.js 123.10 KiB 21:34:40
    dist/assets/js/useDraftStateManager-763f76c5.js 0.60 KiB 21:34:40
    dist/assets/js/index-ab58dd7f.js 5.83 KiB 21:34:40
    dist/assets/js/Layout-27f6b4f1.js 15.30 KiB 21:34:40
    dist/assets/js/index-9aec0134.js 19.06 KiB 21:34:40
    dist/assets/js/index-875af46b.js 13.80 KiB 21:34:40
    dist/assets/js/index-a2b56f04.js 7.03 KiB 21:34:40
    dist/assets/static/index-2c513a2e.css 0.10 KiB 21:34:40
    dist/assets/static/useJobLog-aa48407d.css 0.28 KiB 21:34:40
    dist/assets/static/index-403f002e.css 3.13 KiB 21:34:40
    dist/assets/static/ApplicationSetting-67651d62.css 2.84 KiB 21:34:40
    dist/assets/static/.pnpm-00a09d75.css 23.75 KiB 21:34:40
    dist/assets/static/index-34940da0.css 26.78 KiB 21:34:40
    dist/assets/static/index-38460330.css 5.17 KiB 21:34:40
    dist/assets/static/index-a86ba777.css 2.56 KiB 21:34:41
    dist/assets/static/index-5bffb236.css 6.62 KiB 21:34:41
    dist/assets/static/index-13ae83ed.css 69.26 KiB 21:34:41
    dist/assets/static/index-dbd61d75.css 9.33 KiB 21:34:41
    dist/assets/static/index-a96879e7.css 670.76 KiB 21:34:41
    dist/assets/static/Layout-07cefae2.css 5.32 KiB 21:34:41
    dist/assets/static/style-5167d2f9.css 1013.21 KiB 21:34:41
    dist/assets/js/index-bb83eb50.js 4374.99 KiB

2、构建 & 译时间优化

构建过程耗时从 246.62s 减少到 118 s,耗时减少了 200%+

主要原因是 pnpm 安装依赖更快。

优化前(构建时间):

bash 复制代码
dist/assets/index.a19385eb.js 123.69 KiB / gzip: 35.44 KiB
dist/assets/cssMode.9ee61238.js 243.01 KiB / gzip: 56.09 KiB
dist/assets/index.d726b0d8.js 516.56 KiB / gzip: 151.15 KiB
dist/assets/index.66a838f5.css 1177.79 KiB / gzip: 166.47 KiB
dist/assets/index.9275298d.js 14539.05 KiB / gzip: 3775.69 KiB

(!) Some chunks are larger than 2000 KiB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/guide/en/#outputmanualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.

✨ Done in 246.62s.

优化后(构建时间):

再来看看最终优化后的依赖包大小全景图:

主包的体积已经由优化前的 27.61M 经过拆包优化到 8.8M ,减少了 60%+ ,效果显著~

相关推荐
m0_748247551 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255022 小时前
前端常用算法集合
前端·算法
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203982 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2342 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1233 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~4 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语4 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport4 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg4 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全