When Vite launched, it attracted a lot of attention and NPM downloads went up:

Before Vite, Snowpack also used a no-Bundler build. So how does no-Bundler stack up against the old build tool Webpack? Can smooth migration and perfect substitution be achieved?

Let’s take a look at Vite2, Snowpack3, and Webpack5.

Webpack

Webpack is the most used front-end packaging build tool in recent years, and the community is the most perfect front-end packaging build tool. The 5.x version has optimized the construction details, and the packaging speed has been significantly improved in some scenarios, but it has not solved the problem of slow compilation of large projects, which has been criticized before, which is also related to the mechanism of Webpack itself.

There have been many articles explaining how Webpack works, but we won’t go through them in this article. Let’s focus on the newer ones.

Snowpack

What is Snowpack?

The first tool to take advantage of the browser’s native ESM capabilities was not Vite, but a tool called Snowpack. Formerly known as @pika/ Web, it changed its name to Snowpack starting with version 1.x.

Snowpack describes itself on its website as: “Snowpack is a lightning-fast front-end build tool designed for the modern Web. It is an alternative to a more complex packaging tool such as Webpack or Parcel that has a heavier development workflow. Snowpack utilizes JavaScript’s native module system (called ESM) to avoid unnecessary work and maintain a smooth development experience.

The idea of Snowpack is to reduce or avoid entire bundle packaging, as traditional JavaScript build tools (such as Webpack and Parcel) need to rebuild and repackage the entire bundle of the application each time a single file is saved. Repackaging increases the time between saving your changes and seeing them reflected in the browser. During development **, ** Snowpack provides unbundled Server ** for your apps. ** Each file only needs to be built once to be permanently cached. When a file changes, Snowpack rebuilds the individual file. There is no time wasted in rebuilding each change, just HMR updates in the browser.

Meet Pika, the team that invented Snowpack. The Pika team has an ambitious mission: to make Web apps 90% faster:

To this end, the Pika team developed and maintained two technology architectures: build the related Snowpack and the mass-benefit Skypack.

Many Web applications are built on the basis of different NPM packages, which are bundled by packaging tools such as Webpack. If these NPM packages come from the same CDN address and support cross-domain caching, Then these NPM packages only need to be loaded once during the cache’s lifetime. Other sites using the same NPM package do not need to re-download, but directly read the local cache.

For example, both the official website and b-terminal of Zhaopin are developed based on VUE + VUex. When HR posts are posted on b-terminal, they do not need to re-download their company’s external homepage when entering the official website. They only need to download some business codes related to zhaopin’s official website. To this end, Pika specifically established a CDN (Skypack) for downloading ESM modules on NPM.

Later, when Snowpack was released, the Pika team published A post called “A Future Without Webpack”, telling people to try to ditch Webpack and adopt A new packaged build solution. The image below is taken from their website. It shows the difference between Bundled and unbundled.

With HTTP/2 and 5G networks, we can expect the number of HTTP requests to become less of an issue, and with the popularity of new Web standards, browsers are gradually supporting ESM (< Script Module >).

Source code analysis

The cli method in SRC /index.ts is called when the build is started, with the following truncated code:

import {command as buildCommand} from './commands/build';

export async function cli(args: string[]) {
  const cliFlags = yargs(args, {
    array: ['install'.'env'.'exclude'.'external']})as CLIFlags;

  if (cmd === 'build') {
    await buildCommand(commandOptions);
    return process.exit(0); }}Copy the code

Enter the commands/build file and perform the general logic as follows:

export async function build(commandOptions: CommandOptions) :Promise<SnowpackBuildResult> {
  // Read the config code
  // ...
  for (const runPlugin of config.plugins) {
    if (runPlugin.run) {
      // Execute the plug-in}}// Write the contents of 'import.meta. Env' to a file
  await fs.writeFile(
    path.join(internalFilESbuildLoc, 'env.js'),
    generateEnvModule({mode: 'production', isSSR}),
  );

  // If HMR, load the HMR tool file
  if (getIsHmrEnabled(config)) {
    await fs.writeFile(path.resolve(internalFilESbuildLoc, 'hmr-client.js'), HMR_CLIENT_CODE);
    await fs.writeFile(
      path.resolve(internalFilESbuildLoc, 'hmr-error-overlay.js'),
      HMR_OVERLAY_CODE,
    );
    hmrEngine = new EsmHmrEngine({port: config.devOptions.hmrPort});
  }
 
  // Start building the source file
  logger.info(colors.yellow('! building source files... '));
  const buildStart = performance.now();
  const buildPipelineFiles: Record<string, FileBuilder> = {};

  Install all required dependencies according to the main buildPipelineFiles list, corresponding to section 3 below
  async function installDependencies() {
    const scannedFiles = Object.values(buildPipelineFiles)
      .map((f) = > Object.values(f.filesToResolve))
      .reduce((flat, item) = > flat.concat(item), []);

    // Specify the installation destination folder
    const installDest = path.join(buildDirectoryLoc, config.buildOptions.metaUrlPath, 'pkg');

    / / installOptimizedDependencies method calls the esinstall bag, bag internal calls the rollup and commonjs module analysis the esm
    const installResult = await installOptimizedDependencies(
      scannedFiles,
      installDest,
      commandOptions,
    );

    return installResult
  }

  // Only the comments in the source code are shown below
  // 0. Find all source files.
  // 1. Build all files for the first time, from source.
  // 2. Install all dependencies. This gets us the import map we need to resolve imports.
  // 3. Resolve all built file imports.
  // 4. Write files to disk.
  // 5. Optimize the build.

  // "--watch" mode - Start watching the file system.
  // Defer "chokidar" loading to here, to reduce impact on overall startup time
  logger.info(colors.cyan('watching for changes... '));
  const chokidar = await import('chokidar');

  // Clear buildPipelineFiles when local files are deleted
  function onDeleteEvent(fileLoc: string) {
    delete buildPipelineFiles[fileLoc];
  }

  // Triggered when local files are created and modified
  async function onWatchEvent(fileLoc: string) {
    // 1. Build the file.
    // 2. Resolve any ESM imports. Handle new imports by triggering a re-install.
    // 3. Write to disk. If any proxy imports are needed, write those as well.

    / / triggers HMR
    if (hmrEngine) {
      hmrEngine.broadcastMessage({type: 'reload'}); }}// Create a file listener
  const watcher = chokidar.watch(Object.keys(config.mount), {
    ignored: config.exclude,
    ignoreInitial: true.persistent: true.disableGlobbing: false.useFsEvents: isFsEventsEnabled(),
  });
  watcher.on('add'.(fileLoc) = > onWatchEvent(fileLoc));
  watcher.on('change'.(fileLoc) = > onWatchEvent(fileLoc));
  watcher.on('unlink'.(fileLoc) = > onDeleteEvent(fileLoc));

  // Return some methods for plugin to use
  return {
    result: buildResultManifest,
    onFileChange: (callback) = > (onFileChangeCallback = callback),
    async shutdown() {
      awaitwatcher.close(); }}; }export async function command(commandOptions: CommandOptions) {
  try {
    await build(commandOptions);
  } catch (err) {
    logger.error(err.message);
    logger.debug(err.stack);
    process.exit(1);
  }

  if (commandOptions.config.buildOptions.watch) {
    // We intentionally never want to exit in watch mode!
    return new Promise(() = >{}); }}Copy the code

All modules will be installed by Install. The installation here is to convert modules into ESM and place them in the PKG directory, not the concept of NPM package installation.

Snowpack3 has added some new features that were not supported in previous versions, such as built-in Node service integration, support for CSS Modules, support for HMR, etc.

Vite

What is a Vite?

Vite (The French word for “fast”, pronounced /vit/) is a build tool designed to provide a faster, more streamlined development experience for modern Web projects. It consists of two main parts:

  1. Development server, which provides rich enhancements on the native ESM, such as extremely fast Hot Module Replacement (HMR).
  2. Build command, which builds code using Rollup.

With the release of Vue3, Vite became famous, originally as a packaged build tool for VUe3, and now the 2.x version is released for any front-end framework, not just Vue. In the Vite README, there are references to Snowpack for some ideas.

Vue could have abandoned Webpack in favor of Snowpack, but the decision to develop Vite to build a new wheel was made by the Vue team itself.

The official Vite documentation lists the similarities and differences between Vite and Snowpack. In essence, it explains the advantages of Vite over Snowpack.

Similarities, quoting Vite official words:

Snowpack is also a no-bundle native ESM Dev Server that is very similar in scope to Vite.

Difference:

  1. Snowpack builds are not packaged by default. The advantage is that you can choose Rollup, Webpack and other packaging tools flexibly, but the disadvantage is that different packaging tools bring different experience. Currently, ESbuild packaging is not stable as a production environment, and Snowpack is not officially supported by Rollup. Different tools produce different configuration files;

  2. Vite supports multi-page packaging;

  3. Vite supports Library Mode;

  4. While Vite supports CSS code splitting, Snowpack defaults to CSS in JS.

  5. Vite optimizes asynchronous code loading;

  6. Vite supports dynamic introduction of polyfill;

  7. Vite legacy Mode Plugin, which can generate ESM and NO ESM at the same time.

  8. First Class Vue Support.

In A non-optimized scenario, when A imports an asynchronous block, the browser must request and parse it before A can determine that it also needs A common block C. This results in additional network roundtrips:

Entry ---> A ---> C 
Copy the code

Vite automatically overrides the code-split dynamic import call through the preload step to parallelize the C fetch when A requests:

Entry ---> (A + C) 
Copy the code

It is possible that C will import multiple times, which will result in multiple requests being made without optimization. Vite’s optimization tracks all imports to completely eliminate duplicate requests, as shown below:

First Class Vue Support in point 8, although it is at the bottom of the list, is actually the finishing touch.

Source code analysis

Vite starts with an HTTP server by default if it is not in middleware mode.

export async function createServer(
  inlineConfig: InlineConfig = {}
) :Promise<ViteDevServer> {
  / / get the config
  const config = await resolveConfig(inlineConfig, 'serve'.'development')
  const root = config.root
  const serverConfig = config.server || {}
  
  // Determine if it is middleware mode
  constmiddlewareMode = !! serverConfig.middlewareModeconst middlewares = connect() as Connect.Server
  
  / / middleware model do not create HTTP service, allowing external call with middleware form: https://Vitejs.dev/guide/api-javascript.html#using-the-Vite-server-as-a-middleware
  const httpServer = middlewareMode
    ? null
    : await resolveHttpServer(serverConfig, middlewares)
  
  // Create the WebSocket service
  const ws = createWebSocketServer(httpServer, config)
  
  // Create a file listener
  const{ ignored = [], ... watchOptions } = serverConfig.watch || {}const watcher = chokidar.watch(path.resolve(root), {
    ignored: ['**/node_modules/**'.'**/.git/**'. ignored],ignoreInitial: true.ignorePermissionErrors: true. watchOptions })as FSWatcher


  const plugins = config.plugins
  const container = await createPluginContainer(config, watcher)
  const moduleGraph = new ModuleGraph(container)
  const closeHttpServer = createSeverCloseFn(httpServer)

  const server: ViteDevServer = {
    // Previously defined constants, including: config, middleware, websocket, file listener, ESbuild, etc
  }

  // The listener process is closed
  process.once('SIGTERM'.async() = > {try {
      await server.close()
    } finally {
      process.exit(0)
    }
  })

  watcher.on('change'.async (file) => {
    file = normalizePath(file)

    // Invalidate module diagram cache when file changes
    moduleGraph.onFileChange(file)

    if(serverConfig.hmr ! = =false) {
      try {
        // The general logic is to restart the server directly when modifying the env file and refresh it precisely according to moduleGraph, if necessary
        await handleHMRUpdate(file, server)
      } catch (err) {
        ws.send({
          type: 'error'.err: prepareError(err)
        })
      }
    }
  })

  // Listen for file creation
  watcher.on('add'.(file) = > {
    handleFileAddUnlink(normalizePath(file), server)
  })

  // Listen for file deletion
  watcher.on('unlink'.(file) = > {
    handleFileAddUnlink(normalizePath(file), server, true)})// Mount the plug-in's service configuration hook
  const postHooks: ((() = > void) | void=) [] []for (const plugin of plugins) {
    if (plugin.configureServer) {
      postHooks.push(await plugin.configureServer(server))
    }
  }

  // Load multiple middleware, including CORS, proxy, open-in-Editor, static file service, etc

  // Run the POST hook, applied before the HTML middleware, so that the external middleware can provide custom content instead of index.html
  postHooks.forEach((fn) = > fn && fn())

  if(! middlewareMode) {/ / convert HTML
    middlewares.use(indexHtmlMiddleware(server, plugins))
    / / 404
    middlewares.use((_, res) = > {
      res.statusCode = 404
      res.end()
    })
  }

  ErrorHandler middleware
  middlewares.use(errorMiddleware(server, middlewareMode))

  // Perform optimization logic
  const runOptimize = async() = > {if (config.optimizeCacheDir) {
      // Use ESbuild to package dependencies and write node_modules/.vite/XXX
      await optimizeDeps(config)
      // Update the metadata file
      const dataPath = path.resolve(config.optimizeCacheDir, 'metadata.json')
      if (fs.existsSync(dataPath)) {
        server._optimizeDepsMetadata = JSON.parse(
          fs.readFileSync(dataPath, 'utf-8')}}}if(! middlewareMode && httpServer) {// Override the Listen method and run the optimizer before the server starts
    const listen = httpServer.listen.bind(httpServer)
    httpServer.listen = (async(port: number, ... args: any[]) => {await container.buildStart({})
      await runOptimize()
      returnlisten(port, ... args) })as any

    httpServer.once('listening'.() = > {
      // Update the actual port, as this may be different from the original port
      serverConfig.port = (httpServer.address() as AddressInfo).port
    })
  } else {
    await runOptimize()
  }

  // Finally return to service
  return server
} 
Copy the code

When accessing the Vite service, the default return is index.html:

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <script type="module" src="/@Vite/client"></script>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html> 
Copy the code

Logic to deal with the import file, in the node/plugins/importAnalysis ts file:

export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const clientPublicPath = path.posix.join(config.base, CLIENT_PUBLIC_PATH) let server: ViteDevServer return { name: 'Vite:import-analysis', configureServer(_server) { server = _server }, async transform(source, importer, SSR) {const rewriteStart = date.now () ImportSpecifier[] = [] try { imports = parseImports(source)[0] } catch (e) { const isVue = importer.endsWith('.vue') const maybeJSX = ! IsVue && isJSRequest(importer) // Determine the file postfix to different information const MSG = isVue? `Install @Vitejs/plugin-vue to handle .vue files.` : maybeJSX ? `If you are using JSX, make sure to name the file with the .jsx or .tsx extension.` : `You may need to install appropriate plugins to handle the ${path.extname( importer )} file format.` this.error( `Failed  to parse source for import analysis because the content ` + `contains invalid JS syntax. ` + msg, } // get the code string let s: MagicString | undefined const STR () = = > s | | (s = new MagicString (source)) / / analytical env, glob and handle CJS / / conversion into esm}}}Copy the code

Take the Vue NPM package as an example, the path after the optimizer processing is as follows:

// before
- import { createApp } from 'vue'
+ import { createApp } from '/node_modules/.Vite/vue.runtime.esm-bundler.js?v=d17c1aa4'
import App from '/src/App.vue'

createApp(App).mount('#app') 
Copy the code

What happens when the/SRC/app. vue path in the screenshot is processed by Vite?

First of all, we need to refer to @vitejs /plugin-vue to deal with the internal use of vUE official compiler @vue/ Compiler-sFC, plugin processing logic is the same as rollup plugin, Vite has been extended on the plug-in mechanism of rollup.

Vitejs.dev/guide/ API -p… I’m not doing expansion here.

The compiled app. vue file is as follows:

import { createHotContext as __Vite__createHotContext } from "/@Vite/client";
import.meta.hot = __Vite__createHotContext("/src/App.vue");
import HelloWorld from '/src/components/HelloWorld.vue'

const _sfc_main = {
  expose: [].setup(__props) {
    return { HelloWorld }
  }
}

import { 
  createVNode as _createVNode, 
  Fragment as _Fragment, 
  openBlock as _openBlock, 
  createBlock as _createBlock 
} from "/node_modules/.Vite/vue.runtime.esm-bundler.js?v=d17c1aa4"

const _hoisted_1 = /*#__PURE__*/_createVNode("img", {
  alt: "Vue logo".src: "/src/assets/logo.png"
}, null, -1 /* HOISTED */)

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _hoisted_1,
    _createVNode($setup["HelloWorld"] and {msg: "Hello Vue 3 + Vite"})].64 /* STABLE_FRAGMENT */))}import "/src/App.vue? vue&type=style&index=0&lang.css"

_sfc_main.render = _sfc_render
_sfc_main.__file = "/Users/orange/build/Vite-vue3/src/App.vue"
export default _sfc_main
_sfc_main.__hmrId = "7ba5bd90"
typeof__VUE_HMR_RUNTIME__ ! = ='undefined' && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)
import.meta.hot.accept(({ default: updated, _rerender_only }) = > {
  if (_rerender_only) {
    __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)
  } else {
    __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)
  }
}) 
Copy the code

Can be found, Vite itself will not recursive compilation, the process to the browser, when the browser running to import the HelloWorld from ‘/ SRC/components/HelloWorld. Vue, will initiate a new request, through the middleware to compile the vue, Similarly, to prove our conclusion, we can see the request information for helloworld.vue:

After analyzing the source code, it can be concluded that Snowpack and Vite will take much longer to start up the service than Webpack, but the time from first compilation to full runnability of complex projects will require further testing, which may yield very different results in different scenarios.

Functional comparison

[email protected] [email protected] [email protected]
Support Vue2 Unofficial support:Github.com/underfin/vi… Support: the vue – loader @ ^ 15.0.0 Unofficial support:www.npmjs.com/package/@le…
Support Vue3 support Support: the vue – loader @ ^ 16.0.0 (Github.com/Jamie-Yang/…) Support:www.npmjs.com/package/@Sn…
Support the Typescript Support: ESbuild (default no type checking) Support: ts – loader Support:Github.com/Snowpackjs/…
Supports CSS preprocessors Support:Vitejs. Dev/guide/featu… Support:Vue-loader.vuejs.org/guide/pre-p… Partial support: Only Sass and Postcss are officially available, with unresolved bugs
Support CSS Modules Support:Vitejs. Dev/guide/featu… Support:Vue-loader.vuejs.org/guide/css-m… support
Static file support support support support
The development environment No-bundle Native ESM (CJS → ESM) Bundle (CJS/UMD/ESM) No-bundle Native ESM (CJS → ESM)
HMR support support support
The production environment Rollup Webpack Webpack, Rollup, or even ESbuild
Node API call capability support support support

Compare compile speed at startup

The following set of test code is exactly the same, is Hellow World project, without any complex logic, Webpack and Snowpack respectively introduced the corresponding Vue plugin, Vite does not need any plug-in.

Webpack5 + vue3 (1.62 s)

Project Catalog:

Console output:

Snowpack3 + vue3 (2.51 s)

Project Catalog:

Console output:

Vite2 + vue3 (0.99 s)

Project Catalog:

Console output:

Real project Migration

Test case: An existing complex logic VUE project

After simple testing and investigation results, Snowpack was first excluded from the ecological and performance aspects, and Webpack5 and Vite2 will be tested next.

Issues encountered in migrating Vite2:

1. The. Vue suffix cannot be omitted because the routing mechanism is strongly associated with compilation processing.

2. The. Vue file cannot contain JSX. If the file contains JSX, change the file suffix to.

3. Import {… } from “dayjs” with import duration from ‘dayjs/plugin/duration’

4. When optimizeDeps ignore file path error, node_modules/dayjs dayjs. Main. Js? Version = XXXXX, where version should not be added to query;

The window.$method is not found in the component library, so it cannot strongly depend on the association order, which is related to the request return order.

6. If the dependencies are not written to the cache for the first time, an error is reported and a second restart is required.

7. In the scenario where dependency relationships are complex and Vue is cached for multiple times, ESM secondary encapsulation occurs, that is, nested ESMs in ESM.

For a variety of reasons, debugging has ended here, and the conclusion is that Vite2 is still in its early stages and unstable, and the current moduleGraph mechanism needs to be improved to handle deep dependencies.

Webpack5

The effect is significantly improved compared with the same code tested in Webpack4 for 50+ seconds. There may be errors in the actual scene, but the configuration details of WebpackConfig are basically the same.

Speed up compilation and compression

I don’t know if you have encountered this problem:

<--- Last few GCs ---> [59757:0x103000000] 32063 ms: Mark-sweep 1393.5 (1477.7) -> 1393.5 (1477.7) MB, 109.0/0.0 ms Allocation failure GC in old space requested <-- JS stackTrace --> ==== JS stacktrace ========================================= Security context: 0x24d9482a5ec1 <JSObject> ...Copy the code

Or getting stuck at 92% :

Webpack chunk asset optimization (92%) TerserPlugin 
Copy the code

As the product gets bigger, it takes longer and longer to compile and CI, and one-third or more of that time is spent doing compression. The OOM’s problems also often stem from compression.

How to solve the problem of slow compression and memory, has been an unavoidable topic, Vite uses ESbuild, next analysis of ESbuild.

ESbuild

Below is the official build time comparison chart, which does not describe scenarios, file sizes, etc., so it is not of practical reference value.

The main reason why it is fast is that it should be written in GO and then compiled into Native code. Then, NPM installation dynamically loads the binaries for the corresponding platforms, including Mac, Linux, and Windows, such as Esbuild-Darwin-64.

Es-module-lexer, SWC and so on have the same idea, which are accelerated by compiling into Native code to make up for the shortcoming of Node in intensive CPU computing scenarios.

ESbuild has two functions, Bundler and Minifier. Bundler’s functionality is very different from Babel’s and Webpack’s. Using Bundler directly is risky for your existing business. Minifier can try a production environment compression based on Webpack and Babel products to save the compression time of terser Plugin.

The esbuild-webpack-plugin is also provided for Webpack, so you can use esbuild directly within Webpack.

Advantages and disadvantages and Summary

Snowpack

Disadvantages:

  1. The community was not strong enough to support our subsequent business evolution;
  2. The improvement of compilation speed is not obvious.

Vite

Advantages:

  1. Because of its combination with Rollup, basically all rolllup plug-ins in the community can be used directly, so the community is relatively perfect.
  2. Fast compilation speed.

Disadvantages:

  1. At present, Vite is in the early stage of 2.0, and there are many bugs.
  2. The difference between the local ESbuild and the Babel compilation in the production environment can lead to functional differences.

Webpack5

Advantages:

  1. The actual test is much faster than Webpack4;
  2. Take advantage of ESbuild’s code compression mechanism.

Disadvantages:

  1. The build speed of native development compared to Vite is a write-down (not really a drawback as it addresses production environment differences).

Back to the problem at the beginning of our article, after the migration test above, we need to adjust a large number of codes for Vite migration and adaptation. As the original Vue project does not follow the development paradigm of Vite, the migration process is rather bumpy. As long as the new project follows the official development document of Vite, most of the problems mentioned above will be avoided.

Therefore, the migration cost of existing Webpack projects is still quite high. On the other hand, the difference between local development environment and production environment needs to be paid attention to. If the local development environment adopts no-bundle mechanism, while the production release environment adopts Bundle mechanism, such inconsistency will bring troubles and risks to testing and troubleshooting problems. Prudent attempts are recommended before the ecology is ready.