Vite 深度剖析(四)
- [12. 性能优化](#12. 性能优化)
-
- [12.1 创建项目](#12.1 创建项目)
- [12.2 代码优化(懒加载 + 按需引入 等)](#12.2 代码优化(懒加载 + 按需引入 等))
- [12.3 摇树优化(tree-shaking)](#12.3 摇树优化(tree-shaking))
- [12.4 代码压缩(非常有效,esbuild / terser)](#12.4 代码压缩(非常有效,esbuild / terser))
- [12.5 网络优化](#12.5 网络优化)
-
- [12.5.1 开启 http2](#12.5.1 开启 http2)
- [12.5.2 体验 http2 效果(vite-plugin-mkcert)](#12.5.2 体验 http2 效果(vite-plugin-mkcert))
- [12.6 DNS 预解析(dns-prefetch)](#12.6 DNS 预解析(dns-prefetch))
-
- [12.6.1 方式一(直接手写,不推荐)](#12.6.1 方式一(直接手写,不推荐))
- [12.6.2 方式二(vite-plugin-html 插件,不推荐)](#12.6.2 方式二(vite-plugin-html 插件,不推荐))
- [12.6.3 方式三(自定义插件,不推荐)](#12.6.3 方式三(自定义插件,不推荐))
- [12.6.4 方式四(使用 script 命令,推荐)](#12.6.4 方式四(使用 script 命令,推荐))
- [12.6.5 建议](#12.6.5 建议)
- [12.7 预连接(preconnect)](#12.7 预连接(preconnect))
-
- [12.7.1 为什么 preconnect 看起来"很少被使用"?](#12.7.1 为什么 preconnect 看起来“很少被使用”?)
- [12.7.2 哪些网站在使用 `preconnect`](#12.7.2 哪些网站在使用
preconnect) - [12.7.3 使用方式(直接手写)](#12.7.3 使用方式(直接手写))
- [12.8 预加载(preload)](#12.8 预加载(preload))
- [12.9 prefetch、preconnect、preload 对比](#12.9 prefetch、preconnect、preload 对比)
-
- [12.9.1 功能定位](#12.9.1 功能定位)
- [12.9.2 典型使用场景](#12.9.2 典型使用场景)
- [12.9.3 组合使用建议](#12.9.3 组合使用建议)
- [12.9.4 与其他提示的区别](#12.9.4 与其他提示的区别)
- [12.9.4 优化收益可视化](#12.9.4 优化收益可视化)
- [12.10 构建分析(rollup-plugin-visualizer,目前1.69MB)](#12.10 构建分析(rollup-plugin-visualizer,目前1.69MB))
- [12.11 使用 CDN(极大减小打包体积)](#12.11 使用 CDN(极大减小打包体积))
-
- [12.11.1 严重警告](#12.11.1 严重警告)
- [12.11.2 排除需要使用CDN的模块](#12.11.2 排除需要使用CDN的模块)
- [12.11.3 引入 CDN(方式一:externalGlobals + link)](#12.11.3 引入 CDN(方式一:externalGlobals + link))
- [12.11.4 引入 CDN(方式二:通过别名)](#12.11.4 引入 CDN(方式二:通过别名))
- [12.11.5 修改组件引入方式](#12.11.5 修改组件引入方式)
- [12.11.6 测试(18.52KB,牛啊)](#12.11.6 测试(18.52KB,牛啊))
- [12.12 gzip 压缩](#12.12 gzip 压缩)
-
- [12.12.1 gzip 和 brotli 压缩算法](#12.12.1 gzip 和 brotli 压缩算法)
- [12.12.2 gzip 压缩(vite-plugin-compression2)](#12.12.2 gzip 压缩(vite-plugin-compression2))
- [12.12.3 模拟后端设置 content-encoding](#12.12.3 模拟后端设置 content-encoding)
- [12.13 图片压缩](#12.13 图片压缩)
-
- [12.13.1 插件选择](#12.13.1 插件选择)
- [12.13.2 压缩示例(vite-plugin-image-optimizer)](#12.13.2 压缩示例(vite-plugin-image-optimizer))
- [12.14 性能优化总结](#12.14 性能优化总结)
12. 性能优化
12.1 创建项目
(1)复制一份 vite-vue-code-splitting ,更名为 vite-vue-code-performance。
(2)下载后续可能需要用到的依赖:
shell
pnpm add @vueuse/core
pnpm add echarts
pnpm add vue-echarts
pnpm add @types/echarts -D
pnpm add sass -D
(3)更改 src/views/AboutView.vue(使用 echarts):
typescript
<template>
<div>
<v-chart class="chart" :option="option" />
</div>
</template>
<script setup lang="ts">
import VChart, { THEME_KEY } from "vue-echarts";
import { ref, provide } from "vue";
import "echarts";
// import echarts from "@/utils/echartsRequire";
// import { PieChart } from "echarts/charts";
// echarts.use([PieChart]);
provide(THEME_KEY, "dark");
const option = ref({
title: {
text: "2023报考推荐专业类别",
left: "center",
},
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b} : {c} ({d}%)",
},
legend: {
orient: "vertical",
left: "left",
data: [
"软件工程",
"电子通信",
"电气新能源",
"汉语言学",
"公费师范",
"法学",
"临床八年",
"口腔医学",
"应用经济学",
"数学",
],
},
series: [
{
name: "推荐专业类别",
type: "pie",
radius: "55%",
center: ["50%", "60%"],
data: [
{ value: 735, name: "软件工程" },
{ value: 610, name: "电子通信" },
{ value: 534, name: "电气新能源" },
{ value: 515, name: "汉语言学" },
{ value: 448, name: "公费师范" },
{ value: 248, name: "法学" },
{ value: 210, name: "临床八年" },
{ value: 190, name: "口腔医学" },
{ value: 121, name: "应用经济学" },
{ value: 80, name: "数学" },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)",
},
},
},
],
});
</script>
<style scoped>
.chart {
height: 600px;
}
</style>
(4)更改 src/views/HomeView.vue(使用@vueuse/core):
typescript
<template>
<div class="band">
<el-skeleton style="width: 240px" :loading="isLoading" animated :count="3">
<template #template>
<el-skeleton-item variant="image" style="width: 400px; height: 267px" />
<div style="padding: 14px">
<el-skeleton-item variant="h3" style="width: 50%" />
<div
style="
display: flex;
align-items: center;
justify-items: space-between;
margin-top: 16px;
height: 16px;
"
>
<el-skeleton-item variant="text" style="margin-right: 16px" />
<el-skeleton-item variant="text" style="width: 30%" />
</div>
</div>
</template>
<template #default>
<div v-for="(card, index) in state" :class="`item-${index + 1}`">
<a href="#" class="card">
<div
class="thumb"
:style="`background-image: url(${card.download_url})`"
></div>
<article>
<h1>Lorem ipsum dolor sit amet consectetur.</h1>
<span>{{ card.author }}</span>
</article>
</a>
</div>
</template>
</el-skeleton>
</div>
</template>
<script setup lang="ts">
import { useAsyncState } from "@vueuse/core";
const { state, isLoading } = useAsyncState(async () => {
const res = await fetch("https://picsum.photos/v2/list?page=3&limit=10");
return await res.json();
}, {});
</script>
<style lang="scss">
.band {
width: 90%;
max-width: 1240px;
margin: 8px auto;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto;
grid-gap: 20px;
}
@media only screen and (min-width: 500px) {
.band {
grid-template-columns: 1fr 1fr;
}
.item-1 {
grid-column: 1 / span 2;
}
.item-1 h1 {
font-size: 30px;
}
.item-6 {
grid-column: span 2 / -1;
}
.item-6 h1 {
font-size: 30px;
}
}
@media only screen and (min-width: 850px) {
.band {
grid-template-columns: 1fr 1fr 1fr 1fr;
}
}
/* card */
.card {
min-height: 100%;
background: white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
text-decoration: none;
color: #444;
position: relative;
top: 0;
transition: all 0.1s ease-in;
}
.card:hover {
top: -2px;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.2);
}
.card article {
padding: 20px;
display: flex;
flex: 1;
justify-content: space-between;
flex-direction: column;
}
.card .thumb {
padding-bottom: 60%;
background-size: cover;
background-position: center center;
}
.card p {
flex: 1; /* make p grow to fill available space*/
line-height: 1.4;
}
/* typography */
h1 {
font-size: 20px;
margin: 0;
color: #333;
}
.card span {
font-size: 12px;
font-weight: bold;
color: #999;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 2em 0 0 0;
}
</style>
(5)创建 src/views/HomeView.vue(使用vue3生命周期):
typescript
<template>
<div class="photograph-wall">
<div class="gallery" ref="galleryDom">
<figure v-for="(month, index) in months" :key="index">
<img :src="imgUrl(month.img)" :alt="month.title" :title="month.title" />
<figcaption>{{ month.title }}</figcaption>
<figcaption>{{ month.moral }}</figcaption>
</figure>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, onBeforeUnmount } from "vue";
const months = [
{ img: "1.jpg", title: "首阳", moral: "团圆时刻,美好新生" },
{ img: "2.jpg", title: "绀(gan)香", moral: ""绀"为颜色,清新优美" },
{ img: "3.jpg", title: "莺时", moral: "春光明媚,万物复苏" },
{ img: "4.jpg", title: "槐序", moral: "初夏幽凉,美不胜收" },
{ img: "5.jpg", title: "鸣蜩(tiao)", moral: "清音歌鸣,鲜花烂漫" },
{ img: "6.jpg", title: "季夏", moral: "出梅入伏,炎炎盛夏" },
{ img: "7.jpg", title: "兰秋", moral: "兰吐芬芳,馨香无比" },
{ img: "8.jpg", title: "南宫", moral: "嫦娥住处,瓜果成熟" },
{ img: "9.jpg", title: "菊月", moral: "菊傲秋霜,落叶归根" },
{ img: "10.jpg", title: "子春", moral: "寒易转暖,误以为春" },
{ img: "11.jpg", title: "葭月", moral: '星寒冬月,葭草"绿头"' },
{ img: "12.jpg", title: "冰月", moral: "冰雪堆积,寒梅傲立" },
];
const imgUrl = computed(
() => (filename: string) =>
new URL(`../assets/month/${filename}`, import.meta.url).href
);
const galleryDom = ref<HTMLElement | null>(null);
let timer:NodeJS.Timeout | null = null;
const animStart = () => {
if (galleryDom.value!.classList.contains("active") == false) {
galleryDom.value!.classList.add("active");
timer = setTimeout(() => {
galleryDom.value && animEnd();
}, 10000);
}
};
const animEnd = () => {
galleryDom.value!.classList.remove("active");
};
const animEvent = () => animStart();
onMounted(() => {
window.addEventListener("scroll", animEvent, true);
window.addEventListener("resize", animEvent, true);
});
onBeforeUnmount(() => {
clearTimeout(timer!);
window.removeEventListener("scroll", animEvent, true);
window.removeEventListener("resize", animEvent, true);
})
</script>
<style scoped>
.photograph-wall {
position: relative;
display: flex;
justify-content: center;
align-items: center;
max-width: 100vw;
min-height: 100vh;
overflow-x: hidden;
}
p {
line-height: 1;
}
a {
color: crimson;
text-decoration: none;
}
img {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
}
.gallery {
position: relative;
left: calc(-1 * var(--adjust-size));
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
max-width: 100vw;
padding: 20px;
-webkit-perspective: 0;
perspective: 0;
}
.gallery figure:nth-child(7n) {
--duration: 1s;
--pin-color: crimson;
}
.gallery figure:nth-child(7n + 1) {
--duration: 1.8s;
--pin-color: hotpink;
}
.gallery figure:nth-child(7n + 2) {
--duration: 1.3s;
--pin-color: magenta;
}
.gallery figure:nth-child(7n + 3) {
--duration: 1.5s;
--pin-color: orangered;
}
.gallery figure:nth-child(7n + 4) {
--duration: 1.1s;
--pin-color: darkorchid;
}
.gallery figure:nth-child(7n + 5) {
--duration: 1.6s;
--pin-color: deeppink;
}
.gallery figure:nth-child(7n + 6) {
--duration: 1.2s;
--pin-color: mediumvioletred;
}
.gallery figure:nth-child(3n) {
--angle: 3deg;
}
.gallery figure:nth-child(3n + 1) {
--angle: -3.3deg;
}
.gallery figure:nth-child(3n + 2) {
--angle: 2.4deg;
}
.gallery figure:nth-child(odd) {
--direction: alternate;
}
.gallery figure:nth-child(even) {
--direction: alternate-reverse;
}
.gallery figure {
--angle: 3deg;
--count: 5;
--duration: 1s;
--delay: calc(-0.5 * var(--duration));
--direction: alternate;
--pin-color: red;
position: relative;
display: inline-block;
margin: var(--adjust-size);
padding: 0.5rem;
border-radius: 5px;
box-shadow: 0 7px 8px rgba(0, 0, 0, 0.4);
width: 100%;
height: auto;
text-align: center;
background-color: ghostwhite;
background-size: cover;
background-position: center;
background-blend-mode: multiply;
transform-origin: center 0.22rem;
will-change: transform;
break-inside: avoid;
overflow: hidden;
outline: 1px solid transparent;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.gallery.active figure {
animation-duration: var(--duration), 1.5s;
animation-delay: var(--delay),
calc(var(--delay) + var(--duration) * var(--count));
animation-timing-function: ease-in-out;
animation-iteration-count: var(--count), 1;
animation-direction: var(--direction), normal;
animation-fill-mode: both;
animation-name: swing, swingEnd;
}
.gallery figure:after {
position: absolute;
top: 0.22rem;
left: 50%;
width: 0.7rem;
height: 0.7rem;
content: "";
background: var(--pin-color);
border-radius: 50%;
box-shadow: -0.1rem -0.1rem 0.3rem 0.02rem rgba(0, 0, 0, 0.5) inset;
filter: drop-shadow(0.3rem 0.15rem 0.2rem rgba(0, 0, 0, 0.5));
transform: translateZ(0);
z-index: 2;
}
figure img {
aspect-ratio: 1 /1;
width: 100%;
object-fit: cover;
display: block;
border-radius: 5px;
margin-bottom: 10px;
z-index: 1;
}
figure figcaption {
font-size: 14px;
font-weight: 400;
z-index: 1;
}
figure h2 {
color: crimson;
font-size: 22px;
}
figure p {
font-size: 17px;
}
figure small {
font-size: 12px;
}
figure > div {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
@keyframes swing {
0% {
transform: rotate3d(0, 0, 1, calc(-1 * var(--angle)));
}
100% {
transform: rotate3d(0, 0, 1, var(--angle));
}
}
@keyframes swingEnd {
to {
transform: rotate3d(0, 0, 1, 0deg);
}
}
#info {
position: relative;
text-align: center;
z-index: 1;
}
#info a {
font-size: 1.1rem;
}
/*
@media (orientation: landscape) {
.gallery {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
}
*/
@media (min-width: 800px) {
.gallery {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
}
</style>
(6)src/routers/index.ts 加入照片墙懒加载路由,关键代码:
typescript
{
path: "/photo",
name: "Photo",
component: () => import("@/views/PhotoView.vue"),
},
(7)App.vue 加入照片墙,关键代码:
typescript
<el-menu
:default-active="routePath"
class="el-menu-demo"
mode="horizontal"
router
>
<el-menu-item index="/">首页</el-menu-item>
<el-menu-item index="/student">学生页面</el-menu-item>
<el-menu-item index="/user">用户页面</el-menu-item>
<el-menu-item index="/photo">照片墙</el-menu-item>
<el-menu-item index="/about">关于</el-menu-item>
</el-menu>
(8)添加 src/assets/months 文件夹,并添加 1.jpg - 12.jpg 共12张图片。
(9)vite.config.ts 配置:
typescript
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
vueDevTools(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
build: {
// 是否开启压缩,默认为true
minify: false,
// 可以配置是否关闭css code split
// cssCodeSplit: false,
// 自动注入一个模块与加载器的polyfill
modulePreload: {
polyfill: true,
},
rollupOptions: {
output: {
chunkFileNames: "assets/[name].[hash].js",
entryFileNames: "assets/[name].[hash].js",
assetFileNames: "assets/[ext]/[name].[hash].[ext]",
manualChunks: {
"vue-vendar": ["vue", "vue-router", "@vueuse/core"],
"element-plus": ["element-plus"],
"lodash-es": ["lodash-es"],
echarts: ["echarts", "vue-echarts"],
},
},
},
},
});
(10)pnpm build
typescript
dist/index.html 0.68 kB │ gzip: 0.35 kB
dist/assets/jpg/5.D4XmLVfF.jpg 139.10 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/11.D-CsXvjI.jpg 236.87 kB
dist/assets/jpg/12.BD8LlwrJ.jpg 238.57 kB
dist/assets/jpg/4.Lmoeuf6_.jpg 256.78 kB
dist/assets/jpg/8.BpqUJ-5a.jpg 297.79 kB
dist/assets/jpg/2.BJZ0bpsN.jpg 309.09 kB
dist/assets/jpg/10.CdYkUUr-.jpg 336.22 kB
dist/assets/jpg/6.COzquqWT.jpg 548.69 kB
dist/assets/jpg/1.D0VFAXde.jpg 617.13 kB
dist/assets/jpg/3.C5Zvchlc.jpg 628.58 kB
dist/assets/css/AboutView.DQ19ujAY.css 0.05 kB │ gzip: 0.07 kB
dist/assets/css/PhotoView.BiU3glCU.css 4.49 kB │ gzip: 1.31 kB
dist/assets/css/index.CBpiknsL.css 24.24 kB │ gzip: 4.57 kB
dist/assets/css/TableComp.DTDJVe7j.css 33.39 kB │ gzip: 4.73 kB
dist/assets/TableComp.vue_vue_type_script_setup_true_lang.B4714zeH.js 1.28 kB │ gzip: 0.57 kB
dist/assets/UserView.BrTIijk5.js 1.40 kB │ gzip: 0.70 kB
dist/assets/StudentView.CQEecJgC.js 1.43 kB │ gzip: 0.73 kB
dist/assets/AboutView.BebEwzWG.js 2.31 kB │ gzip: 1.02 kB
dist/assets/PhotoView.D4K7xEud.js 4.61 kB │ gzip: 1.81 kB
dist/assets/index.DC7BuGh6.js 10.44 kB │ gzip: 3.31 kB
dist/assets/lodash-es.DqSfE6Q6.js 65.36 kB │ gzip: 14.41 kB
dist/assets/vue-vendar.BnK9iuAg.js 296.98 kB │ gzip: 69.21 kB
dist/assets/element-plus.BFyWtC36.js 1,824.97 kB │ gzip: 377.50 kB
dist/assets/echarts.CTH3W73p.js 2,719.66 kB │ gzip: 583.78 kB
可以看到,echarts 体积很大
12.2 代码优化(懒加载 + 按需引入 等)
(1)使用路由懒加载;
(2)尽量使用按需引入,比如element-plus的使用。
比如,src/views/AboutView.vue 中的import "echarts";,就可以修改为
typescript
import echarts from "@/utils/echartsRequire";
import { PieChart } from "echarts/charts";
echarts.use([PieChart]);
创建相关文件 src/utils/echartsRequire.ts:
typescript
import * as echarts from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import {
TitleComponent,
TooltipComponent,
LegendComponent,
} from "echarts/components";
echarts.use([
CanvasRenderer,
TitleComponent,
TooltipComponent,
LegendComponent,
]);
export default echarts;
执行 pnpm build
typescript
dist/index.html 0.68 kB │ gzip: 0.35 kB
dist/assets/jpg/5.D4XmLVfF.jpg 139.10 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/11.D-CsXvjI.jpg 236.87 kB
dist/assets/jpg/12.BD8LlwrJ.jpg 238.57 kB
dist/assets/jpg/4.Lmoeuf6_.jpg 256.78 kB
dist/assets/jpg/8.BpqUJ-5a.jpg 297.79 kB
dist/assets/jpg/2.BJZ0bpsN.jpg 309.09 kB
dist/assets/jpg/10.CdYkUUr-.jpg 336.22 kB
dist/assets/jpg/6.COzquqWT.jpg 548.69 kB
kB
dist/assets/AboutView.BAQcdbWd.js 2.50 kB │ gzip: 1.09 kB
dist/assets/PhotoView.CJp9eSGo.js 4.61 kB │ gzip: 1.81 kB
dist/assets/index.B8-YkteU.js 10.44 kB │ gzip: 3.31 kB
dist/assets/lodash-es.DqSfE6Q6.js 65.36 kB │ gzip: 14.41 kB
dist/assets/vue-vendar.BnK9iuAg.js 296.98 kB │ gzip: 69.21 kB
dist/assets/echarts.B1U578R3.js 1,185.24 kB │ gzip: 257.95 kB
dist/assets/element-plus.BFyWtC36.js 1,824.97 kB │ gzip: 377.50 kB
echarts.js 从 2,719.66 kB 降到了 1,185.24 kB
12.3 摇树优化(tree-shaking)
(1)导入具体的函数,不要直接导入全对象(也是按需引入);比如:
typescript
import {debounce} from 'lodash-es';
(2)commonjs模块不支持摇树,尽量使用es模块;
比如下载使用lodash-es,而非lodash
12.4 代码压缩(非常有效,esbuild / terser)
参考:https://vitejs.cn/vite6-cn/config/build-options.html#build-minify
(1)minify:esbuild 默认,压缩速度快,比 terser 快 20-40 倍,压缩率只差 1%-2% ;
(2)minify:terser 压缩率更高 。
(3)当设置为 'terser' 时必须先安装 Terser:
typescript
pnpm add terser -D
(4)vite.config.ts 关键代码,配置如下:
typescript
build: {
// 是否开启压缩,默认为esbuild
// minify: false,
minify: "terser",
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
}
这里的 drop_console 作为硬编码,不是很合理,所以我们要进行进一步改造
(5).env.development:
typescript
# 开发环境端口号
VITE_PORT=8848
# 开发时的API前缀
VITE_API_URL=/api
# 打包时是否自动删除console
VITE_DROP_CONSOLE=false
# 本地(开发)环境
VITE_USER_NODE_ENV=development
(6).env.production:
typescript
# 生成环境端口号
VITE_PORT=8849
# 开发时的API前缀
VITE_API_URL="https://api.xxx.com"
# 打包时是否自动删除console
VITE_DROP_CONSOLE=true
# 线上(生产)环境
VITE_USER_NODE_ENV=production
VITE_IMG_BASE_URL=http://image.yanhongzhi.com
VITE_BUILD_COMPRESS='gzip'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE=true
(7).env.d.ts:
typescript
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare interface ViteEnv {
VITE_APP_TITLE: string;
VITE_PORT: number;
VITE_OPEN: boolean;
VITE_API_URL: string;
VITE_IMG_BASE_URL: string;
VITE_DROP_CONSOLE: boolean;
VITE_BUILD_COMPRESS: "gzip" | "brotliCompress" | "none";
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
VITE_USER_NODE_ENV: string;
}
// 映射类型,将属性设置为只读
type ReadonlyProps<T> = {
readonly [P in keyof T]: T[P];
};
interface ImportMetaEnv extends ReadonlyProps<ViteEnv> {}
(8)完整 vite.config.ts:
typescript
import { fileURLToPath, URL } from "node:url";
import { ConfigEnv, UserConfig, defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import { wrapperEnv } from "./src/build/getEnv";
// https://vite.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
const root = process.cwd();
const env = loadEnv(mode, root);
const viteEnv = wrapperEnv(env);
return {
root,
server: {
open: true,
port: viteEnv.VITE_PORT,
},
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
vueDevTools(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
build: {
// 是否开启压缩,默认为esbuild
// minify: false,
minify: "terser",
terserOptions: {
compress: {
drop_console: viteEnv.VITE_DROP_CONSOLE,
drop_debugger: viteEnv.VITE_DROP_CONSOLE,
},
},
// 可以配置是否关闭css code split
// cssCodeSplit: false,
// 自动注入一个模块与加载器的polyfill
modulePreload: {
polyfill: true,
},
rollupOptions: {
output: {
chunkFileNames: "assets/[name].[hash].js",
entryFileNames: "assets/[name].[hash].js",
assetFileNames: "assets/[ext]/[name].[hash].[ext]",
manualChunks: {
"vue-vendar": ["vue", "vue-router", "@vueuse/core"],
"element-plus": ["element-plus"],
"lodash-es": ["lodash-es"],
echarts: ["echarts", "vue-echarts"],
},
},
},
},
};
});
(9)运行 pnpm build
typescript
✓ 2229 modules transformed.
dist/index.html 0.68 kB │ gzip: 0.35 kB
dist/assets/jpg/5.D4XmLVfF.jpg 139.10 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/11.D-CsXvjI.jpg 236.87 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/11.D-CsXvjI.jpg 236.87 kB
dist/assets/jpg/12.BD8LlwrJ.jpg 238.57 kB
dist/assets/jpg/4.Lmoeuf6_.jpg 256.78 kB
dist/assets/jpg/8.BpqUJ-5a.jpg 297.79 kB
dist/assets/jpg/2.BJZ0bpsN.jpg 309.09 kB
dist/assets/jpg/10.CdYkUUr-.jpg 336.22 kB
dist/assets/jpg/6.COzquqWT.jpg 548.69 kB
dist/assets/jpg/1.D0VFAXde.jpg 617.13 kB
dist/assets/jpg/3.C5Zvchlc.jpg 628.58 kB
dist/assets/css/AboutView.C23tWjFa.css 0.04 kB │ gzip: 0.06 kB
dist/assets/css/PhotoView.CX4OpaOf.css 3.69 kB │ gzip: 1.21 kB
dist/assets/css/index.Bi8cAe8j.css 21.23 kB │ gzip: 4.37 kB
dist/assets/css/TableComp.DCinhtgp.css 33.40 kB │ gzip: 4.73 kB
dist/assets/TableComp.vue_vue_type_script_setup_true_lang._1X4fDpM.js 0.54 kB │ gzip: 0.37 kB
dist/assets/UserView.DCrE8fvY.js 0.73 kB │ gzip: 0.52 kB
dist/assets/StudentView.CBGZik9A.js 0.76 kB │ gzip: 0.55 kB
dist/assets/AboutView.DKBF-Ixl.js 1.33 kB │ gzip: 0.83 kB
dist/assets/PhotoView.j8Sg9Kjz.js 2.59 kB │ gzip: 1.37 kB
dist/assets/index.DydnIS0w.js 5.02 kB │ gzip: 2.39 kB
dist/assets/lodash-es.D4WDArgz.js 25.79 kB │ gzip: 8.82 kB
dist/assets/vue-vendar.tXtx2r8e.js 105.69 kB │ gzip: 39.68 kB
dist/assets/echarts.kg3CLOKd.js 491.34 kB │ gzip: 163.96 kB
dist/assets/element-plus.CNJVbs3s.js 835.45 kB │ gzip: 260.09 kB
build in 16.05s
有趣的是,原来开启压缩后,竟然原来的 echarts 和 element-plus 体积都小了至少一半以上。
我们用 esbuild 压缩试试:
typescript
vite v7.3.1 building client environment for production...
✓ 2229 modules transformed.
dist/index.html 0.68 kB │ gzip: 0.35 kB
dist/assets/jpg/5.D4XmLVfF.jpg 139.10 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/11.D-CsXvjI.jpg 236.87 kB
dist/assets/jpg/12.BD8LlwrJ.jpg 238.57 kB
dist/assets/jpg/4.Lmoeuf6_.jpg 256.78 kB
dist/assets/jpg/8.BpqUJ-5a.jpg 297.79 kB
dist/assets/jpg/2.BJZ0bpsN.jpg 309.09 kB
dist/assets/jpg/10.CdYkUUr-.jpg 336.22 kB
dist/assets/jpg/6.COzquqWT.jpg 548.69 kB
dist/assets/jpg/1.D0VFAXde.jpg 617.13 kB
dist/assets/jpg/3.C5Zvchlc.jpg 628.58 kB
dist/assets/css/AboutView.C23tWjFa.css 0.04 kB │ gzip: 0.06 kB
dist/assets/css/PhotoView.CX4OpaOf.css 3.69 kB │ gzip: 1.21 kB
dist/assets/css/index.Bi8cAe8j.css 21.23 kB │ gzip: 4.37 kB
dist/assets/css/TableComp.DCinhtgp.css 33.40 kB │ gzip: 4.73 kB
dist/assets/TableComp.vue_vue_type_script_setup_true_lang.DlyXlW34.js 0.55 kB │ gzip: 0.38 kB
dist/assets/UserView.B-LpCMuS.js 0.73 kB │ gzip: 0.52 kB
dist/assets/StudentView.Dqe2p3CB.js 0.76 kB │ gzip: 0.55 kB
dist/assets/AboutView.CcCNo-7d.js 1.34 kB │ gzip: 0.84 kB
dist/assets/PhotoView.bU0Ud9jm.js 2.65 kB │ gzip: 1.44 kB
dist/assets/index.IsbSdOP4.js 5.05 kB │ gzip: 2.44 kB
dist/assets/lodash-es.CVJ9dmzo.js 27.38 kB │ gzip: 9.77 kB
dist/assets/vue-vendar.B6bV4KLQ.js 107.20 kB │ gzip: 41.68 kB
dist/assets/echarts.COgEq7ae.js 498.90 kB │ gzip: 169.70 kB
dist/assets/element-plus.C-f7Bnho.js 851.49 kB │ gzip: 274.72 kB
build in 8.65s
发现,确实使用 esbuild 模式进行压缩速度快很多,并且体积只是大了一点。
所以,如果追求极致的压缩大小,就使用terser;
如果对压缩大小没有那么大的需求,建议使用esbuild,压缩速度更快。
12.5 网络优化
12.5.1 开启 http2
(1)HTTP 1.1 协议的问题:
- 队头阻塞的问题:同一个 TCP 管道中同一时刻只能处理一个 HTTP 请求。
- 请求排队的问题:并发请求数量限制,Chrome请求数量超过 6 个时,多出来的请求只能排队、等待发送。
(2)Http2的优点:
- 多路复用:将数据分为多个二进制帧,多个请求和响应的数据帧在同一个 TCP 通道进行传输,解决了之前的队头阻塞问题,同时,HTTP2 协议下,浏览器不再有同域名的并发请求数量限制,因此请求排队问题也得到了解决;
- Server Push:服务端推送能力。可以让某些资源能够提前到达浏览器。
(3)Nginx 的 HTTP2 配置,关键代码:
typescript
http2 on
示例:
typescript
server {
listen 443 ssl;
http2 on;
ssl_certificate server.crt;
ssl_certificate_key server.key;
}
12.5.2 体验 http2 效果(vite-plugin-mkcert)
开发阶段如果想使用http2效果,可以使用库:vite-plugin-mkcert。
中文文档:https://www.npmjs.com/package/vite-plugin-mkcert
(1)下载:
typescript
pnpm add vite-plugin-mkcert -D
(2)vite.config.ts 配置,关键代码:
typescript
import { defineConfig } from 'vite'
import mkcert from 'vite-plugin-mkcert'
export default defineConfig({
server: {
https: true // Vite 5+ 可省略此行,但保留更明确
},
plugins: [mkcert()]
})
(3)这个过程会弹窗提示你保存证书,点击是即可。
(4)运行 pnpm dev。
请求都变成了https请求

(5)可进行自定义(比如域名)等配置
typescript
plugins: [
mkcert({
hosts: ['localhost', 'myapp.test', '192.168.1.100'], // 自定义域名/IP
savePath: './certs', // 证书保存目录
force: true, // 强制重新生成证书
autoUpgrade: true, // 自动升级 mkcert
source: 'coding' // 国内使用 Coding 镜像加速下载 :ml-citation{ref="1" data="citationList"}
})
]
需确保 /etc/hosts(或 Windows 的 C:\Windows\System32\drivers\etc\hosts)中添加了自定义域名指向本地 IP,例如:
typescript
127.0.0.1 myapp.test
然后重新运行pnpm dev,但是我发现浏览器打不开自定义的域名。暂且不管,跳过继续。
12.6 DNS 预解析(dns-prefetch)
dns-prefetch(DNS预获取)是前端网络性能优化的一种措施。它根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短DNS解析时间,进而提高网站的访问速度。


用途 :dns-prefetch:指定被链接资源的域名应该进行DNS预解析。可以提前解析域名,以减少域名解析时间。例如,<link rel="dns-prefetch" href="//example.com">用于DNS预解析域名。
使用场景:它只做 DNS 查询,不建立 TCP 连接,开销小、兼容性好(连 IE11 都支持)。适合那些你确定会访问、但暂时不会加载资源的第三方域名,比如 CDN 域名、统计脚本域名、广告或社交分享接口域名。
适合:特别适合跨域请求多、DNS 解析慢的地区(如某些运营商网络)
注意点:如果域名后续根本不会用到,纯属浪费;如果用了但没配 HTTPS,可能触发不安全警告(现代浏览器通常忽略)
12.6.1 方式一(直接手写,不推荐)
index.html:
html
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 直接手写dns-prefetch -->
<link rel="dns-prefetch" href="//www.baidu.com" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
打包后,仍然会保留。
不建议这么做,以后项目日渐庞大,需要dns-prefetch域名日渐增多,不可能一个一个去记忆。
12.6.2 方式二(vite-plugin-html 插件,不推荐)
(1)下载
typescript
pnpm add vite-plugin-html -D
(2)vite.config.ts 关键代码:
typescript
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { createHtmlPlugin } from 'vite-plugin-html';
export default defineConfig({
plugins: [
vue(),
createHtmlPlugin({
inject: {
data: {
dnsPrefetchLinks: [
{ rel: 'dns-prefetch', href: 'https://cdn.example.com' },
],
},
},
}),
],
});
(3)修改 index.html:
typescript
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<%- dnsPrefetchLinks.map((link) => `<link rel="${link.rel}" href="${link.href}">`).join('\n') %>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
(4)执行 pnpm build,打包后的index.html(格式化后的):
typescript
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="dns-prefetch" href="//jd.com" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Vite App</title>
<script type="module" crossorigin src="/assets/index.BOaUL2Ej.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vue-vendar.BnK9iuAg.js">
<link rel="modulepreload" crossorigin href="/assets/lodash-es.DqSfE6Q6.js">
<link rel="modulepreload" crossorigin href="/assets/element-plus.BFyWtC36.js">
<link rel="stylesheet" crossorigin href="/assets/css/index.CBpiknsL.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
12.6.3 方式三(自定义插件,不推荐)
(1)创建 vite-dns-prefetch.js:
typescript
import type { Plugin } from "vite";
declare type Domain = string;
export default function viteDnsPrefetchPlugin(
dnsPrefetchLinks: Domain[],
): Plugin {
return {
name: "vite-dns-prefetch-plugin", // 唯一标识符
transformIndexHtml(html) {
let resultHtml = html;
const newHTML =
dnsPrefetchLinks
.map((link, index) => {
return `${index ? " " : " "}<link rel="dns-prefetch" href="//${link}">`;
})
.join("\n") + "\n </head>";
resultHtml = resultHtml.replace("</head>", newHTML);
return resultHtml;
// console.log("plugin test");
// return html;
},
};
}
(2)vite.config.js 关键代码:
typescript
import viteDnsPrefetchPlugin from "./src/plugins/vite-plugin-dns-prefetch";
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
// 使用自定义插件
viteDnsPrefetchPlugin(["jd.com", "baidu.com", "google.com"])
]
});
(3)运行 pnpm build
打包后的 index.html:
html
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<script type="module" crossorigin src="/assets/index.BOaUL2Ej.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vue-vendar.BnK9iuAg.js">
<link rel="modulepreload" crossorigin href="/assets/lodash-es.DqSfE6Q6.js">
<link rel="modulepreload" crossorigin href="/assets/element-plus.BFyWtC36.js">
<link rel="stylesheet" crossorigin href="/assets/css/index.CBpiknsL.css">
<link rel="dns-prefetch" href="//jd.com">
<link rel="dns-prefetch" href="//baidu.com">
<link rel="dns-prefetch" href="//google.com">
</head>
<body>
<div id="app"></div>
</body>
</html>
12.6.4 方式四(使用 script 命令,推荐)
之所以推荐这种方式,是因为不需要我们自己去记录需要dns-prefetch的域名,而是通过代码去遍历所有文件的外部域名,最终加到打包后的index.html中。
(1)下载依赖:
typescript
pnpm add node-html-parser -D
pnpm add glob -D
pnpm add url-regex -D
(2)创建 src/scripts/dns-prefetch.ts:
typescript
import * as fs from "fs";
//npm install node-html-parser
//用于解析html文件
import { parse } from "node-html-parser";
//npm install glob
//用于获取文件列表
import { glob } from "glob";
//npm install url-regex
//用于匹配url
import urlRegex from "url-regex";
//获取外部链接正则表达式
const urlPattern = /(https?:\/\/[^\s]+)/i;
//用于存储所有外部链接
const urls = new Set();
//异步搜索所有html文件中的外部链接
async function searchDomain() {
const files = await glob("dist/**/*.{html,js,css}");
for (const file of files) {
//读取文件内容
const source = fs.readFileSync(file, "utf-8");
//匹配外部链接
const matches = source.match(urlRegex({ strict: true }));
if (matches) {
//将匹配到的外部链接添加到urls集合中
matches.forEach((url) => {
const match = url.match(urlPattern);
if (match && match[1]) {
urls.add(match[1]);
}
});
}
}
}
//异步在head中插入链接
async function insertLinks() {
//获取所有html文件
const files = await glob("dist/**/*.html");
//生成链接
const links = [...urls]
.map((url) => `<link rel="dns-prefetch" href="${url}">`)
.join("\n");
//遍历所有html文件
for (const file of files) {
const html = fs.readFileSync(file, "utf-8");
const root = parse(html);
//在head中添加
const head = root.querySelector("head")!;
head.insertAdjacentHTML("afterbegin", links);
fs.writeFileSync(file, root.toString());
}
}
async function main() {
await searchDomain();
await insertLinks();
}
main();
(3)package.json 修改build命令,关键代码:
typescript
"bd": vite build && node ./src/scripts/dns-prefecth.ts
如果是真实项目,就直接修改build语句即可。
(4)运行 pnpm bd
生成的dist/html:
typescript
<!DOCTYPE html>
<html lang="">
<head>
<link rel="dns-prefetch" href="https://vuejs.org/error-reference/#runtime-${type}`;">
<link rel="dns-prefetch" href="http://www.w3.org/2000/svg">
<link rel="dns-prefetch" href="http://www.w3.org/1998/Math/MathML">
<link rel="dns-prefetch" href="http://www.w3.org/1999/xlink">
<link rel="dns-prefetch" href="https://picsum.photos/v2/list?page=3&limit=10">
<link rel="dns-prefetch" href="http://emailregex.com/">
<link rel="dns-prefetch" href="https://element-plus.org/en-US/component/button.html#button-attributes">
<link rel="dns-prefetch" href="https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js">
<link rel="dns-prefetch" href="https://element-plus.org/en-US/component/checkbox.html">
<link rel="dns-prefetch" href="https://element-plus.org/en-US/component/radio.html">
<link rel="dns-prefetch" href="https://github.com/vuejs/vue-next/pull/2485\n">
<link rel="dns-prefetch" href="https://element-plus.org/en-US/component/dialog.html#slots">
<link rel="dns-prefetch" href="https://element-plus.org/en-US/component/drawer.html#slots">
<link rel="dns-prefetch" href="https://element-plus.org/en-US/component/link.html#underline">
<link rel="dns-prefetch" href="https://element-plus.org/zh-CN/component/pagination.html">
<link rel="dns-prefetch" href="https://element-plus.org/en-US/component/scrollbar#infinite-scroll">
<link rel="dns-prefetch" href="http://blogs.adobe.com/webplatform/2014/02/24/using-blend-modes-in-html-canvas/">
<link rel="dns-prefetch"
href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation">
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script type="module" crossorigin src="/assets/index.BOaUL2Ej.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vue-vendar.BnK9iuAg.js">
<link rel="modulepreload" crossorigin href="/assets/lodash-es.DqSfE6Q6.js">
<link rel="modulepreload" crossorigin href="/assets/element-plus.BFyWtC36.js">
<link rel="stylesheet" crossorigin href="/assets/css/index.CBpiknsL.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
12.6.5 建议
滥用风险:虽然DNS预解析可以提升页面加载速度,但过度使用可能会对用户的DNS查询造成不必要的压力,特别是对于那些不经常访问的域名。因此,建议只对那些确实会频繁访问的、重要的资源进行预解析。
12.7 预连接(preconnect)
用途:指定被链接资源应该在页面加载前进行连接。
使用场景 :它不仅查 DNS,还会完成 TCP 握手和 TLS 协商(如果是 HTTPS),代价更高,但收益也更直接。适合页面中明确要加载资源、且对首屏性能影响大的第三方源,比如字体托管服务(fonts.googleapis.com)、核心 API 域名、主 CDN。
使用方式 :同样写在 <head>,例如:<link rel="preconnect" crossorigin href="https://fonts.googleapis.com">
建议配合 crossorigin 属性使用(尤其涉及 CORS 资源时),避免预连接被浏览器忽略
注意:不要滥用------每个 preconnect 都占用一个 HTTP/1.1 连接槽位,HTTP/2 下影响小些,但仍建议控制在 3--6 个以内。

12.7.1 为什么 preconnect 看起来"很少被使用"?
- 使用成本较高:每个 preconnect 会建立完整的 TCP 连接并完成 TLS 握手,消耗浏览器连接槽位和系统资源。若目标域名未实际使用,会造成资源浪费。
- 适用场景有限:仅对关键第三方跨域资源(如 Google Fonts、CDN、API)有效,对同源资源或非关键资源无效甚至有害。
- 开发者认知不足:许多开发者不了解 preconnect 的机制或误以为它适用于所有外部资源。
- 过度使用风险:滥用 preconnect 可能占用浏览器连接池(尤其在 HTTP/1.1 下),反而拖慢页面加载 。
实际上,preconnect 并非"几乎没有网站使用",而是在特定场景下被合理使用,但存在使用门槛和潜在风险,导致其普及度不如其他优化手段(如 preload 或 prefetch)。
12.7.2 哪些网站在使用 preconnect
主流高性能网站和框架已广泛采用 preconnect,尤其在以下场景:
- Google Fonts 集成:官方推荐代码中明确包含两个 preconnect 标签。
- 使用 CDN 的电商、新闻站点:如腾讯、阿里系网站常对静态资源域名使用 preconnect。
- React 等现代框架:提供 preconnect() API,鼓励在组件渲染时按需预连接
12.7.3 使用方式(直接手写)
其实dns-preload的四种方式,preconnect 都可以使用,但是因为它的使用成本较高,通常只用到3个域名左右。比如:
typescript
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="//vuejs.org" crossorigin />
<link rel="preconnect" href="//element-plus.org" crossorigin />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
这个例子其实不太合适,因为我们的资源基本都是本地的,通常需要用到外部静态资源的DNS时,加上对应的preconnect 较为合适。
12.8 预加载(preload)
指定被链接资源应该在页面加载时预加载 。可以用于提前加载页面所需的资源,以加快页面加载速度。例如,<link rel="preload" href="image.jpg" as="image">用于预加载图片资源。
使用方式:返回《11.7.1 依赖分包(manualChunks,推荐,资源变为 modulepreload)》小节查看。
12.9 prefetch、preconnect、preload 对比
| 指标 | dns-prefetch | preconnect | preload |
|---|---|---|---|
| 节省时间 | 20--120 ms(DNS) | 100--500 ms(含连接) | 减少资源下载延迟 |
| 带宽开销 | 极低 | 低 | 高(实际下载资源) |
| 浏览器兼容性 | 最佳(几乎所有浏览器) | 良好(现代浏览器) | 良好(现代浏览器) |
| 推荐数量 | 3--5 条 | ≤2 条 | 仅关键资源 |
12.9.1 功能定位
- dns-prefetch:仅提前完成目标域名的 DNS 解析(将域名转为 IP 地址),不建立连接,也不下载资源。
- preconnect:在 DNS 解析基础上,进一步完成 TCP 握手 和 TLS 协商(HTTPS),相当于建立完整连接,仅差发送 HTTP 请求。
- preload:立即下载指定资源(如字体、JS、CSS 等),并缓存到浏览器,供当前页面使用。
这三者按"准备程度"递进:DNS → 连接 → 资源下载
12.9.2 典型使用场景
-
dns-prefetch
(1)页面中引用了第三方域名(如 Google Fonts、CDN、统计脚本等)。
(2)不确定用户是否一定会访问,但希望减少潜在延迟。
(3)示例:
<link rel="dns-prefetch" href="//cdn.example.com"> -
preconnect
(1)确定即将使用的关键第三方资源(如核心 API、字体服务)。
(2)需配合 crossorigin 属性用于跨域资源(如字体)。
(3)示例:
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> -
preload
(1)当前页面必需的关键资源(如首屏字体、关键 JS/CSS)。
(2)强制高优先级加载,不影响其他资源。
(3)示例:
<link rel="preload" href="critical.css" as="style">
注意:滥用 preconnect 或 preload 会浪费网络和 CPU 资源,应谨慎使用
12.9.3 组合使用建议
最佳实践是分层使用,兼顾兼容性与性能:
typescript
<!-- 对关键第三方域名(如 Google Fonts) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="dns-prefetch" href="https://fonts.gstatic.com"> <!-- 降级兜底 -->
<!-- 对当前页面关键资源 -->
<link rel="preload" href="main-font.woff2" as="font" type="font/woff2" crossorigin>
12.9.4 与其他提示的区别
- prefetch:用于未来页面可能用到的资源,低优先级,在浏览器空闲时下载;
- prerender:预加载并渲染整个页面(类似隐藏 Tab),已逐渐被 Speculation Rules API 取代
12.9.4 优化收益可视化
如需进一步优化,可结合 Google PageSpeed Insights 检测实际收益。
12.10 构建分析(rollup-plugin-visualizer,目前1.69MB)
github 地址:https://github.com/btd/rollup-plugin-visualizer
(1)下载
shell
pnpm add rollup-plugin-visualizer -D
(2)在 vite.config.ts 中引入,关键代码:
typescript
import { ConfigEnv, UserConfig, defineConfig} from "vite";
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
plugins: [
visualizer({
open: true,
filename: "./dist/stats.html",
}),
],
});
(3)运行 pnpm build。
通常来说,会在浏览器自动打开生成的./dist/stats.html。
但是不止为何,我的没有。所以我修改了package.json的build命令:
typescript
"build": "vite build && start dist\\stats.html",
"bp": "pnpm build && vite preview",
再运行pnpm build就会自动打开了。

12.11 使用 CDN(极大减小打包体积)
12.11.1 严重警告
注意:生产环境CDN只能使用私域CDN(就是公司自己的CDN),不要使用公共免费的CDN。
安全比方便更重要。
12.11.2 排除需要使用CDN的模块
(1)vite.config.ts 关键代码:
typescript
import { ConfigEnv, UserConfig, defineConfig} from "vite";
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
build: {
// 是否开启压缩,默认为esbuild
minify: false,
// 自动注入一个模块与加载器的polyfill
modulePreload: {
polyfill: true,
},
rollupOptions: {
external: [
"vue",
"vue-router",
"@vueuse/core",
"element-plus",
"lodash-es",
"echarts",
"vue-echarts",
],
output: {
chunkFileNames: "assets/[name].[hash].js",
entryFileNames: "assets/[name].[hash].js",
assetFileNames: "assets/[ext]/[name].[hash].[ext]",
// manualChunks: {
// "vue-vendar": ["vue", "vue-router", "@vueuse/core"],
// "element-plus": ["element-plus"],
// "lodash-es": ["lodash-es"],
// echarts: ["echarts", "vue-echarts"],
// },
},
},
},
});
(2)scr/views/AboutView.vue 修改echarts引入方式,关键代码:
typescript
import "echarts";
// import echarts from "@/utils/echartsRequire";
// import { PieChart } from "echarts/charts";
// echarts.use([PieChart]);
(3)运行 pnpm bp


此时,项目体积大幅下降,但是因为模块缺失,所以不能运行。
12.11.3 引入 CDN(方式一:externalGlobals + link)
(1)下载相关依赖
typescript
pnpm add rollup-plugin-external-globals -D
(2)vite.config.ts 关键代码:
typescript
import { ConfigEnv, UserConfig, defineConfig} from "vite";
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
build: {
plugins: [
externalGlobals({
vue: "Vue",
"vue-router": "VueRouter",
"element-plus": "ElementPlus",
"@vueuse/core": "VueUse",
echarts: "echarts",
"vue-echarts": "VueECharts",
}),
],
},
},
});
(3)index.html:
typescript
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/element-plus@2.13.6/dist/index.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/vue-echarts@8.0.1/dist/style.min.css"
/>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.31/dist/vue.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@5.0.4/dist/vue-router.global.min.js"></script>
<script src="https://unpkg.com/@vueuse/shared"></script>
<script src="https://unpkg.com/@vueuse/core"></script>
<script src="https://cdn.jsdelivr.net/npm/element-plus@2.13.6/dist/index.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@6.0.0/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-echarts@8.0.1/dist/index.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
12.11.4 引入 CDN(方式二:通过别名)
vite.config.ts 关键代码:
typescript
resolve: {
alias: {
// 注意,别名处理这里只能是ESM模块,CJS模块无法处理
"lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.23/+esm",
},
},
12.11.5 修改组件引入方式
运行 pnpm bp,页面报错。
原因:之前的 element-plus 使用自动引入,省略了 import 会导致使用 CDN 的方式创建vue的组件节点错误。所以,需要将之前的都修改成按需引入(按照页面控制台提示找到对应页面修改即可)。
12.11.6 测试(18.52KB,牛啊)
修改完成后,重新运行pnpm bp


整个应用大小只有18.52KB了。
✓ 36 modules transformed.
dist/index.html 1.28 kB │ gzip: 0.46 kB
dist/assets/jpg/5.D4XmLVfF.jpg 139.10 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/11.D-CsXvjI.jpg 236.87 kB
dist/assets/jpg/12.BD8LlwrJ.jpg 238.57 kB
dist/assets/jpg/4.Lmoeuf6_.jpg 256.78 kB
dist/assets/jpg/8.BpqUJ-5a.jpg 297.79 kB
dist/assets/jpg/2.BJZ0bpsN.jpg 309.09 kB
dist/assets/jpg/10.CdYkUUr-.jpg 336.22 kB
dist/assets/jpg/6.COzquqWT.jpg 548.69 kB
dist/assets/jpg/1.D0VFAXde.jpg 617.13 kB
dist/assets/jpg/3.C5Zvchlc.jpg 628.58 kB
dist/assets/css/AboutView.DQ19ujAY.css 0.05 kB │ gzip: 0.07 kB
dist/assets/css/index.BDnK5i9s.css 1.62 kB │ gzip: 0.69 kB
dist/assets/css/PhotoView.LSfA376v.css 4.49 kB │ gzip: 1.31 kB
dist/assets/TableComp.vue_vue_type_script_setup_true_lang.DGoLAO-E.js 0.96 kB │ gzip: 0.44 kB
dist/assets/UserView.DvVr3K0c.js 1.17 kB │ gzip: 0.60 kB
dist/assets/StudentView.Briv9Bi9.js 1.21 kB │ gzip: 0.64 kB
dist/assets/AboutView.4GbNMW1j.js 2.11 kB │ gzip: 0.96 kB
dist/assets/PhotoView.COjsmzvN.js 4.44 kB │ gzip: 1.74 kB
dist/assets/index.CqGTb3W-.js 9.92 kB │ gzip: 3.09 kB
✓ built in 1.30s
诡异的是,查看构建的结果,和分析报告却大不相同。
12.12 gzip 压缩
12.12.1 gzip 和 brotli 压缩算法
网站资源通常会采用gzip或brotli算法进行压缩。
两者均为无损压缩算法,广泛用于 Web 资源(如 HTML、CSS、JS)的传输优化,但在效率、兼容性、性能等方面存在显著差异.
| 维度 | gzip | brotli |
|---|---|---|
| 压缩率 | 略低 | 比 Gzip 高 17%~25%,尤其对文本类资源效果突出 |
| 压缩速度 | 更快 | 高压缩级别下 Brotli 的 CPU 开销显著更高 |
| 解压速度 | 相近 | 相近 |
| 兼容性 | 几乎所有浏览器和服务器 | 主流现代浏览器均支持,但 IE 不支持 |
| 适用场景 | 老设备从 | 现代浏览器,追求速度 |
静态资源预压缩:对 JS/CSS 等文件在构建阶段生成 .gz 和 .br 版本,避免运行时压缩开销。
避免压缩已压缩格式:如 JPEG、PNG、MP4 等二进制文件,压缩效果微弱甚至增大体积。
压缩级别选择 :
Gzip:推荐 Level 6--7(平衡速度与压缩率);
Brotli:静态资源可用 Level 6--9,动态内容慎用高级别


12.12.2 gzip 压缩(vite-plugin-compression2)

一般来说,压缩都是使用 nginx自带HttpGzip模块 进行配置压缩即可。只是全部交给它会消耗对应的服务器内存。后端其实也能处理。如果都不做的话,可以交给前端,最后nginx进行静态压缩即可。
github :https://github.com/nonzzz/vite-plugin-compression
(1)下载
typescript
pnpm add vite-plugin-compression2 -D
(2)在 vite.config.ts 中配置,关键代码:
typescript
import { ConfigEnv, UserConfig, defineConfig} from "vite";
import { compression } from "vite-plugin-compression2";;
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
build: {
plugins: [
compression({
//压缩算法,默认['gzip', 'brotliCompress']
algorithms: ["gzip"],
//匹配文件
include: [/\.(js)$/, /\.(css)$/],
//压缩超过此大小的文件,以字节为单位
// threshold: 10240,
//是否删除源文件,只保留压缩文件
// deleteOriginalAssets: true
}),
],
},
},
});
(3)运行 pnpm bp
✓ 36 modules transformed.
dist/assets/TableComp.vue_vue_type_script_setup_true_lang.DGoLAO-E.js.gz 0.44 kB
dist/assets/UserView.DvVr3K0c.js.gz 0.60 kB
dist/assets/StudentView.Briv9Bi9.js.gz 0.64 kB
dist/assets/css/index.BDnK5i9s.css.gz 0.69 kB
dist/assets/AboutView.4GbNMW1j.js.gz 0.96 kB
dist/index.html 1.28 kB │ gzip: 0.46 kB
dist/assets/css/PhotoView.LSfA376v.css.gz 1.30 kB
dist/assets/PhotoView.COjsmzvN.js.gz 1.74 kB
dist/assets/index.CqGTb3W-.js.gz 3.09 kB
dist/assets/jpg/5.D4XmLVfF.jpg 139.10 kB
dist/assets/jpg/9.Ck6LtJAu.jpg 203.46 kB
dist/assets/jpg/7.CrP6VT9t.jpg 211.68 kB
dist/assets/jpg/11.D-CsXvjI.jpg 236.87 kB
dist/assets/jpg/12.BD8LlwrJ.jpg 238.57 kB
dist/assets/jpg/4.Lmoeuf6_.jpg 256.78 kB
dist/assets/jpg/8.BpqUJ-5a.jpg 297.79 kB
dist/assets/jpg/2.BJZ0bpsN.jpg 309.09 kB
dist/assets/jpg/10.CdYkUUr-.jpg 336.22 kB
dist/assets/jpg/6.COzquqWT.jpg 548.69 kB
dist/assets/jpg/1.D0VFAXde.jpg 617.13 kB
dist/assets/jpg/3.C5Zvchlc.jpg 628.58 kB
dist/assets/css/AboutView.DQ19ujAY.css 0.05 kB │ gzip: 0.07 kB
dist/assets/css/index.BDnK5i9s.css 1.62 kB │ gzip: 0.69 kB
dist/assets/css/PhotoView.LSfA376v.css 4.49 kB │ gzip: 1.31 kB
dist/assets/TableComp.vue_vue_type_script_setup_true_lang.DGoLAO-E.js 0.96 kB │ gzip: 0.44 kB
dist/assets/UserView.DvVr3K0c.js 1.17 kB │ gzip: 0.60 kB
dist/assets/StudentView.Briv9Bi9.js 1.21 kB │ gzip: 0.64 kB
dist/assets/AboutView.4GbNMW1j.js 2.11 kB │ gzip: 0.96 kB
dist/assets/PhotoView.COjsmzvN.js 4.44 kB │ gzip: 1.74 kB
dist/assets/index.CqGTb3W-.js 9.92 kB │ gzip: 3.09 kB
✓ built in 1.21s
压缩后,css和js都产生了对应的gz文件。

打开打包后的预览页面,发现并没有content-encoding: gzip。这个是需要后端进行设置的。
12.12.3 模拟后端设置 content-encoding
(1)创建 server/app.cjs:
javascript
const http = require('http')
const path = require('path')
const fs = require('fs')
// 提供静态文件服务
const sirv = require('sirv')
const defaultWD = process.cwd()
const publicPath = path.join(defaultWD, 'dist')
const assets = sirv(publicPath, { gzip: true, brotli: true })
function createServer() {
const server = http.createServer()
server.on('request', (req, res) => {
assets(req, res, () => {
res.statusCode = 404
res.end('File not found')
})
})
server.listen(8080, () => {
const { port } = server.address()
console.log(`server run on http://localhost:${port}`)
})
}
function main() {
if (!fs.existsSync(publicPath)) throw new Error('Please check your\'re already run \'npm run build\'')
createServer()
}
main()
(2)下载相关依赖
shell
pnpm add sirv -D
(3)package.json 添加命令
typescript
"server": "node ./server/app.cjs"
(4)运行 npm run server

12.13 图片压缩
12.13.1 插件选择
推荐插件:
(1)需要跨平台兼容或现代格式(WebP/AVIF) → 选 vite-plugin-image-optimizer
(2)需要开发环境转换、精灵图、高级功能 → 选 vite-plugin-image-tools
弃用插件 :
vite-plugin-imagemin 已 不兼容 Vite 5,会导致构建失败。新项目不应该使用;
vite-plugin-minipic
12.13.2 压缩示例(vite-plugin-image-optimizer)
github地址:https://github.com/FatehAK/vite-plugin-image-optimizer
(1)下载依赖
shell
pnpm add vite-plugin-image-optimizer -D
pnpm add sharp -D
pnpm add svgo -D
(2)在 vite.config.ts 中配置,关键代码:
typescript
import { ConfigEnv, UserConfig, defineConfig} from "vite";
import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
build: {
plugins: [
ViteImageOptimizer({
jpg: { quality: 20 },
}),
],
},
},
});
(3)运行 pnpm build
typescript
✨ [vite-plugin-image-optimizer] - optimized images successfully:
dist/assets/jpg/10.CdYkUUr-.jpg -76% 328.34 kB ⭢ 79.08 kB
dist/assets/jpg/11.D-CsXvjI.jpg -75% 231.32 kB ⭢ 58.37 kB
dist/assets/jpg/12.BD8LlwrJ.jpg -81% 232.98 kB ⭢ 46.12 kB
dist/assets/jpg/2.BJZ0bpsN.jpg -75% 301.84 kB ⭢ 76.77 kB
dist/assets/jpg/4.Lmoeuf6_.jpg -83% 250.76 kB ⭢ 44.83 kB
dist/assets/jpg/5.D4XmLVfF.jpg -78% 135.84 kB ⭢ 31.21 kB
dist/assets/jpg/7.CrP6VT9t.jpg -78% 206.72 kB ⭢ 46.73 kB
dist/assets/jpg/8.BpqUJ-5a.jpg -72% 290.81 kB ⭢ 84.21 kB
dist/assets/jpg/1.D0VFAXde.jpg -76% 602.66 kB ⭢ 145.03 kB
dist/assets/jpg/9.Ck6LtJAu.jpg -75% 198.69 kB ⭢ 51.44 kB
dist/assets/jpg/6.COzquqWT.jpg -76% 535.83 kB ⭢ 132.85 kB
dist/assets/jpg/3.C5Zvchlc.jpg -72% 613.85 kB ⭢ 172.93 kB
💰 total savings = 2960.04kB/3929.63kB ≈ 75%
可以看到,针对图片内容,总体积压缩了75%左右。
12.14 性能优化总结
(1)下载 构建分析工具 rollup-plugin-visualizer。直观查看项目各个资源大小,后续针对性调整。
(2)性能优化手段分类,大致为三类:代码优化、网络优化、资源优化。
(3)路由懒加载(必做)。打包时进行分包,查看页面时只加载当前页面资源。
(4)按需导入(必做)。组件或者工具库使用按需导入,有利于摇树优化(没用到的组件和函数自动删除),极大减少体积。
注意:有个坑,element-pluus 使用自动导入时(如果没有写按需引入组件的代码),如果后续改成CDN优化,会导致引入组件失效,从而vue无法正确生成组件节点,导致报错 。所以始终建议,手写按需引入组件的代码。
(5)使用 es 模块库(必做) 。比如使用 lodashe-es 替代 lodash,有利于摇树优化。
(6)合理使用资源标识符。dns-prefetch(DNS 预解析)、preconnect(提前建立外域通道)、preload(重要资源预加载)。
(7)代码分割(必做) 。将vue、element-plus 等第三方依赖拆分出来单独构建打包。一方面并行加载有利于减少初始加载时间,另一方面有利于长期缓存。
(8)使用私域 CDN。一定要使用公司自己的CDN,没有的话,千万别用公网的CDN。安全可用比性能优化更重要。
另外,使用CDN和代码分割有一定冲突。因为使用CDN后,对应的第三方依赖就是整体请求,就不存在所谓的代码分割了。
(9)gzip/br 压缩。可做可不做,一般运维或者后端处理。前端也可做,减少服务器内存消耗。通常不删除原文件,这样浏览器不支持时,可以去加载原文件。
(10)图片压缩。针对不同后缀图片进行不同程度压缩。质量会有所下降,看取舍。
(11)选择合适插件。随着构建工具的版本变化,往往对应优化插件可能会失去效果(作者为爱发电,后续没有升级)。所以需要经常根据构建结果判断,是否修改使用插件,选择更符合当前版本的插件更为重要。
vite 系列文章: