Vite 深度剖析(四)

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.js2,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

有趣的是,原来开启压缩后,竟然原来的 echartselement-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 压缩算法

网站资源通常会采用gzipbrotli算法进行压缩。

两者均为无损压缩算法,广泛用于 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

压缩后,cssjs都产生了对应的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)代码分割(必做) 。将vueelement-plus 等第三方依赖拆分出来单独构建打包。一方面并行加载有利于减少初始加载时间,另一方面有利于长期缓存。

(8)使用私域 CDN。一定要使用公司自己的CDN,没有的话,千万别用公网的CDN。安全可用比性能优化更重要。

另外,使用CDN和代码分割有一定冲突。因为使用CDN后,对应的第三方依赖就是整体请求,就不存在所谓的代码分割了。

(9)gzip/br 压缩。可做可不做,一般运维或者后端处理。前端也可做,减少服务器内存消耗。通常不删除原文件,这样浏览器不支持时,可以去加载原文件。

(10)图片压缩。针对不同后缀图片进行不同程度压缩。质量会有所下降,看取舍。

(11)选择合适插件。随着构建工具的版本变化,往往对应优化插件可能会失去效果(作者为爱发电,后续没有升级)。所以需要经常根据构建结果判断,是否修改使用插件,选择更符合当前版本的插件更为重要。

vite 系列文章:

Vite 深度剖析(一)

Vite 深度剖析(二)

Vite 深度剖析(三)

Vite 深度剖析(四)

相关推荐
薛定e的猫咪2 小时前
多智能体强化学习求解 FJSP 变体全景:动态调度、AGV 运输、绿色制造与开源代码导航
人工智能·学习·性能优化·制造
摸鱼仙人~8 小时前
从0到1实现LLM服务极限压测:精准计算首字延迟(TTFT)与性能优化百分比
性能优化
黄宝良8 小时前
FreeSWITCH入门到精通系列(七):FreeSWITCH 性能优化与调优实战
性能优化
MU在掘金916959 小时前
一个CLI工具的架构是怎么搭起来的
性能优化·开源
Sheldon一蓑烟雨任平生9 小时前
Vite 深度剖析(二)
vite·静态资源处理·hmr·css工程化处理·模块热替换·vite 插件
Sheldon一蓑烟雨任平生10 小时前
Vite 深度剖析(一)
vue·react·vite·环境变量·esbuild·vite.config.ts·依赖预构建
Beginner x_u11 小时前
前端八股整理(工程化 01)|Git 常见命令、rebase/merge、pull/fetch 与前端性能优化
前端·性能优化·git 常见命令
南村群童欺我老无力.12 小时前
鸿蒙ForEach渲染列表的唯一性约束与性能优化
华为·性能优化·harmonyos
CSharp精选营12 小时前
.NET 11 Preview 3 发布:C# 15 union 类型终补齐,Kestrel 暴增 40%
云原生·性能优化·ai开发·.net11·csharp15