preface

Some time ago, Vite did an optimization for Dependency pre-bundling. In a nutshell, it means that the dependencies that need to be precompiled are compiled by Vite before the DevServer starts, and then the compiled dependencies are applied dynamically when the module’s imports are analyzed.

I think one of the first questions you might ask is: isn’t Vite a No Bundle? It’s true that Vite is No Bundle, but relying on precompilation does not mean that Vite is going to be a Bundle. Let’s not be too quick to define it, because its existence must have a real value.

So, today, this article will focus on the following three points with you from the question point, a deep understanding of the Vite dependency precompilation process:

  • What is dependency precompilation
  • Depends on precompilation
  • Dependency on precompiled implementation (source analysis)

What is dependency precompilation

When you reference Vue and lodash-es in your project, you’ll see output like this on the terminal when you start Vite:

This means that Vite has pre-compiled dependencies on the Vue and lodash-es that you introduce in your project! Here’s a look at the dependency precompilation of Vite in plain English:

  • By default, Vite places production dependencies in package.jsondependenciesPart of the DevServer enables dependency precompilation, that is, the dependency is compiled first, and then the compiled file is cached in memory (under node_modules/.vite), and the cached content is directly requested when the DevServer is started.
  • Configure it in the vite.config.js fileoptimizeDepsYou can chooseYes or noThe name of the dependency that is precompiled, and Vite will use this option to determine whether the dependency is precompiled.
  • Added at startup--forceOptions, which can be usedForced toDo dependency precompilation.

Note that forced rereliance on precompilation means to ignore previously compiled files and recompile directly.

So, going back to the question at the beginning of the article, we can understand dependency precompilation here as an optimization, i.e. No Bundle can do without it, it’s better with it! Also, relying on precompilation doesn’t make bricks without straw; Vite was inspired by Snowpack.

So, what is the purpose of relying on precompilation? What is the meaning of optimization

Second, rely on the role of precompilation

For the role of relying on precompilation, the Vite official also made a detailed introduction. So, here we combine the legend to understand, specifically will be two points:

1. Compatible with CommonJS and AMD module dependencies

Because the DevServer of Vite is based on the browser’s Natvie ES Module implementation, so for the use of dependencies if CommonJS or AMD modules, then need to carry out Module type conversion (ES Module).

2. Reduce the number of requests caused by dependent references between modules

Usually we introduce some dependencies, and it will itself have some other dependencies. The official documentation gives a classic example of this when we use lodash-es on a project:

import { debounce } from "lodash-es"

If we open the Network panel of the Dev Tool in the page without relying on precompilation:

You can see that there are approximately 600+ lodash-es-related requests at this point, and all of them took 1.11 seconds to load. Is that okay? Now, let’s look at a case where dependency precompilation is used:

At this point, there was only one lodash-es-related request (precompiled), and all the requests took 142 ms to load, a sevenfold reduction in time! The time saved here is what we often call cold start time.

So, so far we have seen the concept and role of Vite dependencies on precompilation. I think you’re all curious about how does this work? Next, let’s dive into the ite source code to take a closer look at the dependency precompilation process!

Relying on precompiled implementations

In the Vite source code, the default dependency precompilation process occurs before DevServer is turned on. Here, again, we use the example of introducing Vue and Lodash-es dependencies into the project.

Note that the following source-related functions are taken
The core logicExplanation (pseudocode).

3.1 Dev Server before startup

First, Vite creates a DevServer, which is the local development server we normally use. This process is done by the createServer function:

// packages/vite/src/node/server/index.ts async function createServer( inlineConfig: InlineConfig = {} ): Promise<ViteDevServer> { ... // Normally we will hit the logic if (! MiddlewareMode && HttpServer) {// Overwrite the listen of DevServer. Const listen = HttpServer.list.bind (HttpServer) HttpServer.listen = (async (port: number,... args: any[]) => { try { ... // Depend on precompiled related await runOptimize()}... }) as any ... } else { await runOptimize() } ... }

You can see that before DevServer actually starts, it calls the runOptimize function for any dependency precompilation related processing (a simple rewrite with bind).

RunOptimize function:

/ / packages/vite/SRC/node/server/index. The ts const runOptimize = async () = > {/ / config. OptimzizeCacheDir refers to node_modules/.vite if (config.optimizeCacheDir) { .. try { server._optimizeDepsMetadata = await optimizeDeps(config) } .. server._registerMissingImport = createMissingImpoterRegisterFn(server) } }

The runOptimize function is responsible for calling and registering the optimizeDeps function that relies on precompilation. Specifically, it does two things:

1. Do dependency precompilation

The optimizeDeps function is the core function of the Vite implementation that depends on precompilation. It is precompiled for the first time according to the optimizeDeps option that configures Vite. It returns the object generated after parsing the node_moduels/.vite/_metadata.json file (including the location of the pre-compiled dependencies, the location of the original file, and so on).

_metadata. Json file:

{ "hash": "bade5e5e", "browserHash": "830194d7", "optimized": { "vue": { "file": "/ Users/WJC/Documents/FE/demos/vite2.0 - demo/node_modules /. Vite/vue js", "SRC" : "/ Users/WJC/Documents/FE/demos/vite2.0 - demo/node_modules/vue/dist/vue runtime. The esm - bundler. Js", "needsInterop" : False}, "lodash - es" : {" file ":"/Users/WJC/Documents/FE/demos/vite2.0 - demo/node_modules /. Vite/lodash - es. Js ", "SRC" : "/ Users/WJC/Documents/FE/demos/vite2.0 - demo/node_modules/lodash - es/lodash js", "needsInterop" : false}}}

Here, let’s take a look at the meaning of these four attributes:

  • hashGenerated from the contents of a file that needs to be precompiled, to prevent the same dependencies from being compiled over and over again when DevServer starts, i.e. the dependencies have not changed and do not need to be recompiled.
  • browserHashhashAnd additional dependencies found at run time, used to invalidate browser requests for precompiled dependencies.
  • optimizedContains properties for each precompiled dependency that describe the path to the dependency source filesrcAnd the compiled pathfile.
  • NeedsInterop is mainly used for dependency import analysis in Vite, which is handled by the TransformCJSImport function in the ImportAnalysisPlugin. It rewrites dependent import code that needs to be precompiled for CommonJS. For example, when we use React in a Vite project:

    import React, { useState, createContext } from 'react'

    NeedsInterop is true, so the importAnalysisPlugin will rewrite the code imported into React:

    import $viteCjsImport1_react from "/@modules/react.js";
    const React = $viteCjsImport1_react;
    const useState = $viteCjsImport1_react["useState"];
    const createContext = $viteCjsImport1_react["createContext"];

    The reason for this rewrite is that CommonJS modules do not support named exports. So, without the plugin conversion, you will see an exception like this:

Uncaught SyntaxError: The requested module '/@modules/react.js' does not provide an export named 'useState'

Those of you who are interested in learning more about this can check out this PR
https://github.com/vitejs/vit…, here will not do too detailed introduction ~

2. Register dependent precompiled related functions

Call createMissingImpoterRegisterFn function, it will return a function, its internal will still call precompiled optimizeDeps function, just different from precompiled process for the first time, a newDeps at this point, is the new require precompiled dependency.

Obviously, then, both the first precompile and the subsequent precompile are implemented by calling the OptimizeDeps function. So, let’s take a look at the OptimizeDeps function

3.2 Precompiled implementation of the core OptimizeDeps function

OptimizeDeps function is defined in the packages/vite/node/optimizer index. The ts, it is responsible for precompiled process depends on:

// packages/vite/node/optimizer/index.ts export async function optimizeDeps( config: ResolvedConfig, force = config.server.force, asCommand = false, newDeps? : Record<string, string> ): Promise<DepOptimizationMetadata | null> { ... }

Because OptimizeDeps has a lot of internal logic, we break it down into five steps:

1. Read the file information of the dependency at this time

Since it is a compile dependency, it is obvious that each compile needs to know the Hash value of the file content at that time, so that the dependency can be recompiled when the dependency changes, so that the latest dependency content can be applied.

So, the getDefHash function is called to get the dependent Hash value:

DepOptimizationMetadata = {hash: const mainHash = getDefHash (root, config) const data: depOptimizationMetadata = {hash: mainHash, browserHash: mainHash, optimized: {} }

And for
dataThese three properties have been introduced above and will not be discussed again here

2. Compare the Hash of the cached file

We also mentioned earlier that if the –force Option is used when Vite is started, a re-dependency precompile is forced. So, when it is not a –force scenario, the process of comparing the old and new dependent Hash values is performed:

// Default to false if (! Force) {let prevData try {prevData = JSON.parse(fs.readFileSync(dataPath, dataPath); If (prevData && prevData.hash === data.hash) {log(' hash is consistent.skipping. Use ') {if (prevData && prevData.hash === data.hash) {log(' hash is consistent.skipping --force to override.') return prevData } }

You can see that if the Hash values of the old and new dependencies are the same, the content of the old dependency is returned directly.

3. Invalid or uncached cache

If the Hash above is not equal, then the cache is invalid, so the cache folder is deleted. Or if the cache is not cached at this time, and the precompiled logic is relied on for the first time (the cache folder does not exist), then the cache folder is created:

if (fs.existsSync(cacheDir)) {
    emptyDir(cacheDir)
  } else {
    fs.mkdirSync(cacheDir, { recursive: true })
  }

And the thing to notice here is that
cacheDirThis refers to the node_modules/.vite folder

When we talked about DevServer startup earlier, we mentioned that there are two types of precompilation: first precompilation and subsequent precompilation. The difference between the two is that the latter passes in a newDeps, which represents a new dependency that needs to be precompiled:

let deps: Record<string, string>, missing: Record<string, string> if (! newDeps) { ; ({deps, missing} = await scAnimPorts (config))} else {// If you assign newDeps to deps deps = newDeps missing = {}}

Also, you can see that for the former, the first precompile, the scAnimPorts function is called to find the dependency deps associated with the precompile, which would be an object:

{lodash - es: '/ Users/WJC/Documents/FE/demos/vite2.0 - demo/node_modules/lodash - es/lodash js' Vue: '/ Users/WJC/Documents/FE/demos/vite2.0 - demo/node_modules/vue/dist/vue runtime. The esm - bundler. Js'}

Missing represents a dependency not found in node_modules. So, when missing exists, you get this reminder:

scanImportsInside the function is a call to a name called
dep-scanThe internal plugins of. I’m not going to talk about it here
dep-scanThe specific implementation of the plug-in, interested students can understand by themselves

Then, back to the above logic for the latter (when newDeps exists), it is relatively simple. Deps will be directly assigned as newDeps, and missing is not needed. Because newDeps will be passed in only after the subsequent import and installation of new dependencies, there will be no missing dependencies at this time (the ImportAnalysisPlugin built-in in Vite will filter out these in advance).

4. Handle optimizedeps.include dependencies

Earlier, we mentioned that the dependencies that need to be compiled are also determined by the optimizeDeps option of vite.config.js. So, after dealing with dependencies, you’ll need to deal with optimizeDeps.

If optimizeDeps.iclude (array) does not exist, it will throw an exception: optimizeDeps.iclude (array) :

const include = config.optimizeDeps? .include if (include) { const resolve = config.createResolver({ asSrc: false }) for (const id of include) { if (! deps[id]) { const entry = await resolve(id) if (entry) { deps[id] = entry } else { throw new Error( `Failed to resolve force included dependency: ${chalk.cyan(id)}` ) } } } }

5. Use esbuild to compile dependencies

Then, after the above processing related to precompiled dependencies (file hash generation, precompiled dependency determination, etc.). The final step of dependency precompilation is to use esbuild to compile the corresponding dependency:

. const esbuildService = await ensureService() await esbuildService.build({ entryPoints: Object.keys(flatIdDeps), bundle: true, format: 'esm', ... })...

The EnSureService function is the encapsulated util of Vite. It essentially creates an esbuild service that uses the Service. build function to complete the compilation process.

The flatIdDeps parameter passed in is an object created from the dependencies collected by the deps mentioned above. Its purpose is to provide entries for esbuild compilation. The flatIdDeps object is:

{lodash - es: '/ Users/WJC/Documents/FE/demos/vite2.0 - demo/node_modules/lodash - es/lodash js' My moment: '/ Users/WJC/Documents/FE/demos/vite2.0 - demo/node_modules/moment/dist/moment. Js' Vue: '/ Users/WJC/Documents/FE/demos/vite2.0 - demo/node_modules/vue/dist/vue runtime. The esm - bundler. Js'}

Well, at this point we have analyzed the entire implementation that relies on precompilation 😲 (manually for anyone who sees this).

Then, after the startup of DevServer, when the module needs to request a pre-compiled dependency, the ResolvePlugin inside Vite will resolve whether the dependency exists in the “seen” (” seen “will store the compiled dependency mapping). If so, it can directly apply the corresponding compiled dependencies in the node_modules/.vite directory to avoid directly requesting the pre-compiled dependencies, thus shorting the cold start time.

conclusion

By understanding the role and implementation of Vite dependencies on precompilation, I think we should stop worrying about bundles or No bundles. It’s still the same thing. Also, relying on precompilation can be an interesting question to ask in an interview scenario 😎. Finally, if there is something wrong or wrong in the article, please Issue it

Thumb up 👍

If you get anything from reading this article, you can like it. It will be my motivation to keep sharing. Thank you

I am Wu Liu, like innovation, tinkering with source code, focus on source code (VUE 3, VET), front-end engineering, cross-end technology learning and sharing. In addition, all my articles will be included in
https://github.com/WJCHumble/Blog, welcome Watch Or Star!