Understanding Vite's Dependency Scanning and Pre‑Bundling Process
Vite improves dev‑server startup and runtime speed by scanning every project HTML file (excluding node_modules) with an esbuild plugin that traverses the module graph, classifies JavaScript, assets, bare imports and framework files, creates virtual modules, and records each bare import’s actual path for pre‑bundling.
Vite performs a dependency pre‑bundling step to improve compatibility and performance. The core of this step is a dependency scan that collects all bare imports from the project.
The scan starts by treating every HTML file (excluding node_modules ) as an entry point. Vite uses esbuild with a custom plugin ( esbuildScanPlugin ) to traverse the module graph.
Key parts of the plugin:
import { build } from 'esbuild';
export async function scanImports(config) {
let entries = await globEntries('**/*.html', config);
const deps = {};
const missing = {};
const plugin = esbuildScanPlugin(config, container, deps, missing, entries);
await Promise.all(entries.map(entry =>
build({
absWorkingDir: process.cwd(),
write: false,
entryPoints: [entry],
bundle: true,
format: 'esm',
plugins: [...plugins, plugin],
})
));
return { deps, missing };
}During resolution, the plugin distinguishes several module types:
JS modules – handled directly by esbuild.
Non‑JS assets (CSS, images, etc.) – marked as external.
Bare imports – modules whose IDs are not paths. If they resolve to a file inside node_modules , they are recorded in deps and treated as external.
HTML/Vue/Svelte/… files – resolved to a virtual html namespace, then loaded by extracting <script> tags.
HTML loading example:
build.onResolve({ filter: /\.(html|vue|svelte|astro)$/ }, async ({ path, importer }) => {
const resolved = await resolve(path, importer);
if (!resolved || resolved.includes('node_modules')) return;
return { path: resolved, namespace: 'html' };
});
build.onLoad({ filter: /\.(html|vue|svelte|astro)$/, namespace: 'html' }, async ({ path }) => {
let raw = fs.readFileSync(path, 'utf-8');
raw = raw.replace(commentRE, '
');
const scriptRE = isHtml ? scriptModuleRE : scriptRE;
let js = '';
let scriptId = 0;
let match;
while ((match = scriptRE.exec(raw))) {
const [, openTag, content] = match;
const srcMatch = openTag.match(srcRE);
if (srcMatch) {
const src = srcMatch[1] || srcMatch[2] || srcMatch[3];
js += `import ${JSON.stringify(src)}\n`;
} else if (content.trim()) {
const key = `${path}?id=${scriptId++}`;
scripts[key] = { loader, content };
const virtualModulePath = virtualModulePrefix + key;
js += `export * from ${virtualModulePath}\n`;
}
}
return { loader: 'js', contents: js };
});Virtual modules are resolved later:
build.onResolve({ filter: /^virtual-module:.*/ }, ({ path }) => ({
path: path.replace(virtualModulePrefix, ''),
namespace: 'script',
}));
build.onLoad({ filter: /.*/, namespace: 'script' }, ({ path }) => scripts[path]);The final deps object maps each bare import name to its real file path, e.g.:
{
"vue": "D:/app/vite/node_modules/.pnpm/[email protected]/node_modules/vue/dist/vue.runtime.esm-bundler.js",
"lodash-es": "D:/app/vite/node_modules/.pnpm/[email protected]/node_modules/lodash-es/lodash.js"
}This scan determines which dependencies Vite will pre‑bundle, improving dev‑server start‑up time and runtime performance.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.