前言
最近做的需求是海外的项目,使用的 vite2.x
和 vue3
来开发,在第一版提测的时候,发现打包之后,总包大小有 4M
多,有很大的优化的空间。
下面我将从以下几个维度给大家讲下如何优化一个vite
项目的:👇🏻
工具链说明
- 目前
vite2.x
是基于rollup
打包的,而不是esbuild
,详见这里 - 使用
rollup-plugin-visualizer
进行打包分析,打包之后,会在根目录默认生成一个stats.html
文件 - 使用 manualChunks 对主包行配置分割,包括
css
和js
文件(核心工具包和第三方依赖包)等 - 使用
vite-plugin-imagemin
进行图片压缩。每次打包都会压缩一次,会占用构建耗时,有值得优化的空间,此处先不作讨论 @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'],
},
});
};
打包信息的输出如下:👇🏻
从打包输出信息我们可看出:
assets/index.43ae9aaf.js
打包之后 14499.98 KiB / gzip 之后仍然有 3732.16 KiB- 个别图片资源过大:
bash
dist/assets/banner.051258d3.png 1423.75 KiB
- 构建用了
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-es
和lodash
,前者是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-imagemin
(github.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-imagemin 和 imagemin 写了一个脚本,可以实现如下功能:
- 打包之前自动执行这个脚本进行图片压缩
- 压缩之后生成一个
imagemin.map.json
文件,是记录哪些图片已经压缩过了,不需要二次压缩 - 压缩之前检查当前图片的
修改时间
值是否在imagemin.map.json
文件内,在的话则过滤走,不在才需要压缩 - 压缩之后的图片,覆盖原本路径的处理
- 有些图片在压缩之后反而比原图更大了,对这些图片不做覆盖原图处理,直接保留原图,但是需要在
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; console.log(filePath + '不在缓存入列');
tinyMap.set(filePath, {
mtimeMs
});
return true;
// 系统时间戳,比Date.now()更精准,多了小数点后三位,所以控制在1ms内都认为是有效缓存
} else {
if (Math.abs(ctimeMs - mtimeMs) > 1) {
debug &amp;&amp;
console.log(`
${filePath}在缓存但过期了而入列,${ctimeMs} ${mtimeMs} 相差${
ctimeMs - mtimeMs
}`);
tinyMap.set(filePath, {
mtimeMs
});
return true;
} else {
// debug &amp;&amp; console.log(filePath + '在缓存而出列');
return false;
}
}
});
debug &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
。如果后面添加了新图片,也只对新图片进行压缩。
这种方案也有缺点:
- 覆盖原图,在设置压缩插件参数的时候,不方便看效果,需要先备份好
- 对指定目录下匹配通配符后缀的图片文件都会进行压缩,而不是业务组件引用了哪些图片,哪些图片才需要压缩
关于 imagemin
的备注:
imagemin
这个包需要在node >= 12.20.0
执行;在国内下载会比较慢,需要在package.json
加字段"resolutions": { "bin-wrapper": "npm:bin-wrapper-china" }
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.
使用 安装依赖 & 图片压缩 & 构建编译总耗时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 ,相当于纯安装依赖 & 构建时间耗时约为 166s
总结
通过打包分析,确认了图片压缩导致了构建时间长(79.07 s),确认了以下因素导致了主包过大(共 568.19 kiB(gziped) , vendor
530.72 KiB(gziped) ,index
37.47 KiB(gziped) ):
lodash
&lodash-es
全量打包css
全量打包到一块legacy
包存在冗余@antv/g2plot
和@antv/g6
大包vendor
巨大- 图片资源多
解决方案:
- 通过
manualChunks
方案把lodash
&lodash-es
独立打包并按需打包,减少主包体积 css
根据公共样式、字体图标样式、原子 css 方法库等拆包- 通过去掉冗余配置,减轻
legacy
包 @antv/g2plot
和@antv/g6
不打包,而是作为外部依赖external
出去- 调整依赖包打包策略:
(1)优先按照模块分开打包,大模块独立打包
(2)小模块按照兜底策略走合成打包
- 通过脚本,在构建之前压缩图片,并通过缓存避免下次重复压缩,优化构建过程
八、成果:
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%+ ,效果显著~