Vite 分包预加载优化:首屏延迟与后续交互的平衡

2025年8月1日Elecmonkey

背景:为什么会卡?

现代前端 JavaScript Bundle 的体积不断膨胀,为了优化首屏加载性能,常会使用代码分割策略。每当有一个路由页面被拆分为动态 import 导入的模块,Vite 就会自动将其拆分成独立的 chunk 文件,只有当用户访问到该页面时才加载。

这在大型项目中几乎是必然必要的——功能复杂的应用不可能把所有代码打在入口的 JS Bundle 里让用户等着慢慢下载。

typescript
1lazy(() => import('./pages/XXXPage.tsx'))

但是如果你的应用不大,碰巧服务器延迟又很高,就有可能出现首屏没快多少,每次切换 router 跳转时页面就会有明显卡顿——因为被 split 出去的 JS Bundle 就是在第一次被访问到的时候才下载的呀!

解决思路:预加载所有页面模块

解决策略非常直接:

在首屏渲染完成后,后台空闲时自动拉取其余页面的 JS chunk。

这样用户虽然第一次不会访问这些页面,但 JS 提前缓存了,下次跳转时就不会卡。

Vite 的 import.meta.glob

Vite 提供了一个极其实用的语法糖:

typescript
1const modules = import.meta.glob('./pages/*.tsx')

这段代码的含义是:

  • Vite 在构建时记录所有 ./pages/ 下的 .tsx 文件路径;
  • 每个路径对应一个懒加载函数(返回 import());

利用 import.meta.glob 收集模块

typescript
1// 收集所有页面组件 2const modules = import.meta.glob('./pages/*Page.tsx'); 3 4export function preloadAllPages(exceptFile: string = '') { 5 for (const path in modules) { 6 if (exceptFile && path.endsWith(exceptFile)) continue; 7 // 给主页一个排除自己的机会 8 9 modules[path]().then(() => { 10 console.log(`预加载成功: ${path}`); 11 }).catch((error) => { 12 console.warn(`预加载失败: ${path}`, error); 13 }); 14 } 15}

在主页调用

typescript
1import { preloadAllPages } from './routes-preload'; 2import { onMount } from 'solid-js'; // 用 Solid.js 举个例子 3 4const App: Component = () => { 5 onMount(() => { 6 setTimeout(() => { 7 preloadAllPages('MainPage.tsx'); // 首屏页面,不需要重复加载 8 }, 500); // 页面稳定后延迟触发 9 }); 10 11 return ( 12 // 路由组件... 13 ); 14};

import.meta.glob 的匹配时机

构建后没有 .tsx 文件了,那 import.meta.glob('./pages/*.tsx') 还能匹配到什么?

import.meta.glob 是 Vite 提供的 API,很显然它不是最终在运行时运行的函数,而是在构建阶段解析并生成模块映射表,写死在构建产物的代码中。

编译后的效果类似如下:

javascript
1const modules = { 2 './pages/HomePage.tsx': () => import('./assets/HomePage.hash1234.js'), 3 './pages/AboutPage.tsx': () => import('./assets/AboutPage.hash5678.js'), 4}

我们执行对应的函数就把模块 import 进来了,import 了浏览器当然要下载对应的文件。当然,动态 import() 需要浏览器支持,IE 11 之类的就不要想了。这也是为什么 import.meta.glob 不支持动态传参(如变量路径)的原因。

Qwik 哲学

假如,假如项目非常非常大,不巧的是我们又对性能非常敏感。主页直接拉所有 lazy 了的模块感觉有点吃力,还有没有更好的策略呢?

Qwik 作为一个极致拆包的框架,必然要在 prefetch 策略上狠下功夫——首屏时间不能以牺牲后面所有每一步的交互为代价,对吧?进入视口 prefetch、鼠标 hover prefetch…… 反正 Vite 把能力提供到了,后面一切逻辑都可以自己发挥创造了!