In the first part, we had an overview of Vite's features and how to implement it. Now, we will try to get an abstract of Vite's logic. I hope this will provide few answers to the question "how it works", and will help the reader to avoid implementation pittfalls.
Nb: this article only concerns Vite in dev mode. For production, Vite uses bundling so we will not detail the logic here.
I. Vite's 'unbundled' logic:
For a definition of 'unbundled development', please have a look on Part I, section I.
A. Browser ESM implementation: the import process
Even though there are several specificities to the browser ESM implementation, to better apprehen Vite's logic we will focus on how browsers import modules.
For exhaustive information on browser ESM implementation, have a look at this awesome in-depth article from the Mozilla Team, which also uncovers how this works internally.
So browsers expose now a way to import modules from an HTML entry point: the
script tag with the new
module value to the
<script type="module" src="./main.js"></script>
Why do we need to add the
modulevalue ? Because modules differ from regular scripts and have to be treated according to - see here for more details on this.
Then, inside an imported module, browsers support the
import App from “/js/app.js"
As with the
script tag, this triggers an HTTP request to load the file
app.js (which will be by default treated as a module and not as a sccript). This allows to easily import nested dependencies, as we are used to do with bundled development.
As soon as there are few layers of dependencies, module imports can lead to a cascade of HTTP requests. Therefore, this not suitable for production (a performance comparison in the article prevously mentionned here )
Any new module fetch is stored in a dictionary where it is keyed by URL. This implies that any module is only loaded once (as long as it is re-imported with the same module specifier).
An last point to highlight: you may have noticed that, in our examples, the module specifier always start with
/. This is because, for now, bare imports are not supported by browsers, so we must use URLs or relative module paths.
B. Vite's logic in action:
Nb: This part only treats about Vite's logic in development mode. In production mode, Vite still uses bundle.
As we explained in part I, the idea is to use the native ESM support to load and instantiate the source code, instead of serving bundles.
To visualize it, I think the best is an example. Here is the top-level module (the entry point) of a standard Vue (3) app, as served to the browser by Vite:
<script type="module" src="./main.js"></script>
And that’s all. As you can see, this is surprisingly close to the source code (here only module specifiers have been changed).
So everything will be, layer by layer, imported from here via a cascade of module imports (HTTP requests).
Once the whole dependency graph has been imported, the browser instantiates module from the bottom (modules which do not have dependencies) to up (here the upper script). So, in our example, all the dependencies are ready when Vue bootstrap process starts.
II. Code transformation and dependencies management:
How does Vite deals with resources which are not modules?
A. Most of resources will be compilled on the fly:
As mentioned in part I, Vite compiles any imported resource on the fly into ES module. These code transformations are handled by a series of Koa middlewares, or 'plugins', which somehow remind of Webpack loaders.
Here is the list of all Vite built-in plugins. For example:
vuePlugin, which transforms
.vuefiles (it relies on the Vue compiler to parse SFC (Single File Components) and to produce render functions
moduleRewritePlugin, which rewrites module specifiers to be browser-friendly and performs some HMR-related operations (details on this below).
Vite exposes an API to let users add their own plugins (actually a transform function which is then turned into a plugin by Vite), which will be added at the (almost) top of the plugins pipe.
This article shows how to write a custom plugin
B. Dependencies management:
For dependencies which are supposed to run in the browser (non-dev dependencies), Vite will look for an ESM distribution, or, if there is none, it will prebundle the dependency it into ESM module (with Rollup support).
Vite being a node application, CJS distributions are welcomed for dev-dependencies (dependencies used during development such as testing, building, ...).
In rare cases, it may happen though that Vite fails to convert a dependency into ES module. In that case, Vite would exit with a clear warning or the app may break in the runtime, failing at finding some modules.
This is due to the difference of structure between static and dynamic modules (see C.). This generally happens with nested modules, which are more difficult to statically analyze.
C. Why is transpillation to ESM harder:
ESM have, by design, a static structure. This means that all dependencies have to be imported before the execution of the module, which cannot import dependency on the runtime. This also implies that module specifiers have to be plain string, and cannot depend on the script execution (the module specifiers can't be variables).
Actually it may import modules dynamically, with the
import()function, but this will be done asynchronously.
Whereas dynamic module formats can synchronously import modules in the runtime. Which implies that imports may relie on inputs which might be available only on the script exectution. And this is what makes the conversion into a static structure difficult.
For more information and examples, you may have a look on this article which shows very concretely the difference between static and dynamic structures.
So is there any plan for Vite to give a 100% support to this functionality?
According to this issue, it will not, because it would require to implement some tricky 'hacks' (code compilation and runtime helpers to handle the dynamic logic) which, in addition to being a bit dubious, are far from Vite's primary design (to work with ESM). So the team choose to rather push the ecosystem forward, by promoting ES distributions.
See part I for few workarounds to get through this dependency problem.
III. Instantaneous browser updates:
Disclaimer: this part is addressed to the reader curious about the logic behing Vite's updates, and does not contain much practical information.
A. Instantaneous updates factors:
2 factors explain why Vite can update so quickly a running app on source code update:
- HMR support for Vue, React and Svelte (with plugins)
- lighter code transformation (due to unbundled development)
HMR, or Hot Module Replacement, is a feature enabling to live-update modules of a running application, without requiring a full reload (and often also retaining it's state). For more information, here is a HMR spec proposition which also give an overview of HMR.
Concerning HMR an important thing to understand is that Vite does not implement library or framework related HMR code itself (excepted, to some extend, for Vue, see below), but only implements the 'common part' of HMR process and exposes an HMR API, which make it (almost) completely framework agnostic.
B. HMR overview:
Here is an overview of how this works:
- on start, Vite creates a 'hot context' (a HMR context): Vite adds a
hotproperty in the import.meta object (ESM space to add meta properties to a module) of modules referencing HMR. This object exposes the API to configure HMR for the module. At the same time, Vite creates a graph (a "map") of importers/importees relationships, which it will use during updates.
.vuefiles do not need to consume the HMR API because their 'HMR configuration' is handled natively by Vite
- still on start, enables HMR in the browser by shipping a script which will, in the runtime, listen and implement handlers for hmr-related events (which will be sent by the server). These handlers basically 1. asynchronously import modules notified by the server 2. handle these modules according to their hot context
- on code change, Vite checks which module should be reimported (thanks to the 'importers/importees graph') and notifies the browser.
For this, Vite checks if the changed module accepts HMR, and, if not, it walks up the whole chain of importer until it finds, for each import, a module accepting HMR. In this case, all these modules will have to be updated in the app. If there is an import chain where no module accepting HMR has been found, the whole app would have to be reloaded.
For a more detailled explanation on how HMR works, you can have a look here
As mentionned previously, the "browserside" HMR part (the 'callback' part which, at the end of the process, gets as input an updated module and updates the running app), is by design out of Vite scope and relies on external dependencies: Vue's HMR script for Vue, react-refresh package for React, etc...
Thanks for reading so far ! For now, Vite is very young and experimental, and uses concepts that we are not really used to inour daily developments - I tried to make these as clear as possible, I hope this was not too stodgy !
Anyways, I am personally (and I hope you as well!) looking forward to future developments, and integration in existing projects. And perhaps, at some point into frameworks.
Anyways, there is no doubt on one thing: Vite will improve developers experience.