Vite性能优化指南 ✅
目录
1️⃣ 树摇
2️⃣ 分包
3️⃣ 多入口打包
4️⃣ 其他优化手段
树摇
“树摇”这玩意并非某个构建工具的专利,应该说基于ESM模块化开发的项目都有能力进行树摇。详细的分析可见煮啵的另一篇文章中的最后一小节,最核心的原因是CJS的require
本质上是一个运行时的函数。此处只介绍树摇带来的具体效果:
// utils/index.js
export function util1() {console.log('util1')
}
export function util2() {console.log('util2')
}// main.js 入口文件
import { util1 } from './utils/index.js
console.log(util1)
这样的代码在打包后会摒弃未被使用的util2
,只保留被使用的util1
。但如果用CJS模块化进行改写:
// utils/index.js
exports.util1 = () => console.log('util1')
exports.util2 = () => console.log('util2')// main.js 入口文件
const { util1 } = require('./utils/index.js')
这种写法在打包后,即使util2
未被使用,但也会带打包产物中被保留。
分包
在PDD的三面中被狠狠拷打🤯
“分包”即所谓的“代码分割”。常用的实现分包的方法有两种:通过修改Vite配置文件进行分包,或者通过动态导入语法import()
进行代码分割。
通过修改Vite配置文件
设想一下:在我们的项目中引入了lodash
,在进行分包前,lodash相关的代码会和业务代码一起被打包到同一个chunk中,且chunk的命名是通过哈希的方法来生成的,只要业务代码发生改动,那么chunk的命名几乎就会改变。
问题是,每一次更新我们修改的只是业务代码,lodash这些第三方包的代码我们肯定是不会改动的,但是每一次更新(甚至是每一次刷新)浏览器都得重新请求一次lodash的代码。因为lodash所在的chunk的名称会随版本的更新而变化,对这个chunk的请求浏览器走不了缓存。
所以我们的需求很简单:有没有办法让lodash被打包到一个单独的chunk中,后续浏览器只在第一次加载的时候请求这个chunk即可,后续都读缓存,以提高响应速度?有的兄弟,有的🤓🤓,以下介绍最基本的分包方法(详细的配置可见rollup的文档):
// vite.config.js
import { defineConfig } from "vite";
export default defineConfig({build: {minify: false, // 取消默认的压缩行为,方便观察打包产物rollupOptions: {output: {manualChunks: (id, { getModuleInfo }) => {// getModuleInfo可以查看当前模块的信息if (id.includes('node_modules/lodash')) return "vendor/lodash"}}}},
})
通过import()动态导入
import()
动态导入也是实现代码分割的方法,这种优化本质上是借助Promise
实现的,详细的分析见煮啵的另一篇博文的最后一章。import动态导入常用于实现路由懒加载(组件按需引入)。以React为例,见以下Demo:
// LazyComponent.tsx
export default function LazyComponent() {return <div>LazyComponent</div>;
}// App.tsx
import { lazy } from "react";
// 动态(按需)引用懒加载组件
const LazyComponent = lazy(() => import("./LazyComponent/index"));
const App = () => {return (<div><div>App</div><LazyComponent /></div>);
};
export default App;
之后不管是Chrome控制台还是本地打包产物,都可以看见LazyComponent
组件相关的代码被抽离为一个单独的chunk。
多入口打包
多入口打包允许你将一个项目拆分为多个独立的入口文件(如前台index.html和后台admin.html),每个入口生成单独的依赖图和资源包。这种优化方法更适合多页面应用MPA,不适合常见的SPA应用。~~煮啵用的是在是不多。~~假设现在前端股工程的目录如下:
App/
|—src
|——scriptA.js # 脚本A
|——scriptB.js # 脚本B
|——utils.js # 工具函数
|—indexA.html # 入口A
|—indexB.html # 入口B
|—vite.config.js
代码如下:
// utils.js
export function sayHello() {console.log('Hello')
}
export function sayGoodbye() {console.log('Goodbye')
}// scriptA.js
import { sayHello } from "./utils";
function scriptA() {console.log("scriptA is running");sayHello();
}
scriptA();// scriptB.js
import { sayHello, sayGoodbye } from "./utils";
function scriptB() {console.log("scriptB is running");sayHello();sayGoodbye();
}
scriptB();// vite.config.js
export default defineConfig({build: {minify: false, // 取消默认的压缩行为,方便观察打包后的产物rollupOptions: {input: {scriptA: path.resolve(__dirname, 'src/indexA.html'),scriptB: path.resolve(__dirname, 'src/indexB.html'),},}},
})
此时Vite会以scriptA.js和scriptB.js这两个文件为入口,打包的产物有三个脚本文件,分别为:
// scriptA-HashCode.js
import { s as sayHello } from "./index-BVlMXCZJ.js";
function scriptA() {console.log("scriptA is running");sayHello();
}
scriptA();// scriptB-HashCode.js
import { s as sayHello, a as sayGoodbye } from "./index-BVlMXCZJ.js";
function scriptB() {console.log("scriptB is running");sayHello();sayGoodbye();
}
scriptB();// index-HashCode.js
(function polyfill() {// ...额外的与Vite相关的代码。
})()
function sayHello() {console.log("Hello");
}
function sayGoodbye() {console.log("Goodbye");
}
export {sayGoodbye as a,sayHello as s
};
此处Vite已进行了默认的优化。默认情况下被引用了至少两次的文件,会被打成一个独立的包。因为sayHello
被多次复用,在构建中次函数所属的文件会被单独抽离出来。如果sayHello
只在一处被调用,那么打包后它会在调用处的chunk中被声明。
其他优化手段
图片转Base64
关于Base64的分析见煮啵另一篇博文的最后一章。Vite默认将小于4KB的图片转为Base64格式,也可以自己手动调整:
// vite.config.js
export default {build: {assetsInlineLimit: 4096, // 调整阈值},
};
开启资源压缩
Vite默认 (没有手动配过,一直用的是默认) 使用 Terser 压缩 JS,ESBuild 压缩 CSS:
// vite.config.js
export default {build: {minify: 'terser', // 或 'esbuild'(更快但压缩率略低)terserOptions: { // 自定义 Terser 配置compress: { drop_console: true },},},
};
暂时只想到这么多。后续有新的再来补充🧊