{T}

开发期页面闪烁与依赖预构建优化

概述

后台管理项目里大量页面使用 Element Plus 并结合自动导入插件按需引入组件,开发时第一次点击「基础表格」「复杂表格」「基础表单」等页面,常常会出现轻微白闪:控制台还在解析新的依赖和样式。这类问题的本质往往不是路由本身慢,也不是页面组件写得差,而是 Vite 在首次访问该模块时才开始补做依赖预构建和样式注入——浏览器先渲染出页面结构,再等依赖和样式补齐,视觉上就表现为白闪或闪烁。

本文从 Vite 与 Webpack 的开发期差异讲起,说明 optimizeDeps.include 如何预热高频依赖,如何用 glob 批量覆盖 Element Plus 的深层样式导入,以及如何避免把「开发期预构建」和「生产构建样式 external」混为一谈。

学习目标

  • 区分「样式未就绪导致的白闪」和「页面自身的过渡动画」
  • 理解 Vite 原生 ESM 按需加载与 Webpack 整包构建的开发期差异
  • optimizeDeps.include 预热高频依赖,减少首次切页白闪
  • 用 glob 模式一次性覆盖 element-plus/es/components/**/style/css 这类深层导入
  • 明确 optimizeDeps 主要作用于开发期,生产样式 external 由入口导入和 CDN 策略决定

一、白闪的本质:首次访问才触发的依赖预构建

页面切换时的白闪,在使用 Element Plus、自动导入插件、路由按页面切分模块的项目里非常常见。根因是:当前页面依赖此前没有被预构建,组件样式也还没有进入缓存,浏览器先渲染出页面结构,再等依赖和样式补齐。

判断方法很直接:首次访问某页面明显闪烁,第二次访问变顺滑,通常就说明依赖缓存已经生效。这类问题主要发生在开发环境,不代表生产环境一定有同样表现,也不要把它和页面自身的过渡动画混淆。

二、Vite 与 Webpack 的开发期差异

理解这个问题前,要搞清楚两者处理依赖的方式不同:

  • Webpack 开发期会先把依赖打进 bundle,再通过 HMR 做增量替换;
  • Vite 开发期基于原生 ESM 提供源码,请求到哪个模块就处理哪个模块,并自动用 esbuild 做 dependency pre-bundling。

这也是为什么 Vite 首次启动通常更快,但第一次访问某些全新路由时,仍可能遇到「冷加载」——新深层导入会触发额外的预构建。不要把「Vite 很快」理解成「任意新路由第一次访问都零成本」。

三、optimizeDeps.include 预热高频依赖

optimizeDeps.include 是一类典型的开发体验优化配置。当你已经观察到某些依赖会在点开页面时才被处理,就可以把它们显式塞进 include,让 Vite 在更早的阶段就把这些依赖准备好。在这个案例里,最值得优先放进去的是 dayjs 和 Element Plus 的某些深层样式导入。

ts
import { defineConfig } from "vite";

export default defineConfig({
  optimizeDeps: {
    include: [
      "dayjs",
      "element-plus/es/components/table/style/css",
      "element-plus/es/components/form/style/css"
    ]
  }
});

include 不是越多越好,应优先放「高频访问且容易触发闪烁」的依赖。调整后如果效果不明显,先检查缓存是否还在沿用旧结果。

四、用 glob 批量覆盖深层样式导入

当项目里还有大量组件页、后续还会继续访问新组件时,手写几十条 style/css 路径显然不现实。Vite 官方文档已明确:optimizeDeps.include 支持 trailing glob pattern,可以一次性覆盖大量深层导入。

ts
import { defineConfig } from "vite";

export default defineConfig({
  optimizeDeps: {
    include: [
      "dayjs",
      "element-plus/es/components/**/style/css"
    ]
  }
});

这里的 glob 主要解决「深层导入很多、不断触发重新预构建」的问题。当前官方推荐直接用 Vite 支持的 glob,不一定非要自己额外引 fast-glob

五、optimizeDeps 主要是开发期能力,不要让它背生产 external 的锅

课程里提到「生产环境不应该再包含这些 style/css」,工程思路本身没错,但需要结合当前 Vite 版本说得更准确:optimizeDeps 主要用于开发期 dependency optimizer,从 Vite 5.1 开始,构建阶段的依赖预构建实验能力已经被移除。所以:

  • optimizeDeps 不会直接决定生产打包是否把 CSS 打进去;
  • 生产环境样式是否外部化,主要看入口导入方式、CDN 配置和构建策略。

如果想在配置里区分开发和生产,更推荐写成「开发时启用 optimizeDeps,生产时不额外声明它」,这更多是出于配置清晰度,而不是为了控制生产打包结果。推荐用 command === "serve" 来表达「开发期优化」,比 isProd 更贴近 Vite 配置语义。

ts
import { defineConfig } from "vite";

const devOptimizedDeps = [
  "dayjs",
  "element-plus/es/components/**/style/css"
];

export default defineConfig(({ command }) => {
  const isServe = command === "serve";

  return {
    optimizeDeps: isServe
      ? { include: devOptimizedDeps }
      : undefined
  };
});

六、改完配置记得清理预构建缓存

改完 vite.config.ts 后,如果效果没生效,最常见的原因是 node_modules/.vite 仍在使用旧缓存。Vite 会把预构建依赖缓存到这里,不清缓存,浏览器和本地都可能继续使用旧的预构建结果。官方给出的常见做法有两类:删除 node_modules/.vite,或使用 vite --force

bash
rm -rf node_modules/.vite
pnpm dev
bash
pnpm vite --force

正确操作顺序:修改 vite.config.ts → 删除 node_modules/.vite → 重启 dev server → 重新进入相关页面验证闪烁是否消失。如果浏览器也有强缓存,可顺手做一次硬刷新。


常见问题

问题原因解决方案
某个页面第一次打开会白闪,第二次明显顺滑首次访问时才触发依赖预构建和样式缓存将相关依赖加入 optimizeDeps.include
element-plus 写进 include 还是会闪真正频繁触发的是深层样式导入,而非包名本身element-plus/es/components/**/style/css
改完 vite.config.ts 没效果node_modules/.vite 仍在使用旧缓存删除缓存或使用 vite --force 后重启
把开发期白闪和生产构建体积混为一谈optimizeDeps 与生产 external 不是同一条链路开发期看依赖预构建,生产期看入口导入和 CDN 策略
只优化了表格页,其他组件页还是闪include 覆盖范围不完整优先改成 glob,而不是继续手写单个组件路径

延伸阅读