Skip to content

Dependency Pre-Bundling

When you run wite for the first time, you may notice this message:

Pre-bundling dependencies:
  react
  react-dom
(this will be run only when your dependencies or config have changed)

The Why

This is Wite performing what we call "dependency pre-bundling". This process serves two purposes:

  1. CommonJS and UMD compatibility: During development, Wite's dev serves all code as native ESM. Therefore, Wite must convert dependencies that are shipped as CommonJS or UMD into ESM first.

    When converting CommonJS dependencies, Wite performs smart import analysis so that named imports to CommonJS modules will work as expected even if the exports are dynamically assigned (e.g. React):

    // works as expected
    import React, { useState } from 'react'
    
  2. Performance: Wite converts ESM dependencies with many internal modules into a single module to improve subsequent page load performance.

    Some packages ship their ES modules builds as many separate files importing one another. For example, lodash-es has over 600 internal modules! When we do import { debounce } from 'lodash-es', the browser fires off 600+ HTTP requests at the same time! Even though the server has no problem handling them, the large amount of requests create a network congestion on the browser side, causing the page to load noticeably slower.

    By pre-bundling lodash-es into a single module, we now only need one HTTP request instead!

NOTE

Dependency pre-bundling only applies in development mode, and uses esbuild to convert dependencies to ESM. In production builds, @rollup/plugin-commonjs is used instead.

Automatic Dependency Discovery

If an existing cache is not found, Wite will crawl your source code and automatically discover dependency imports (i.e. "bare imports" that expect to be resolved from node_modules) and use these found imports as entry points for the pre-bundle. The pre-bundling is performed with esbuild so it's typically very fast.

After the server has already started, if a new dependency import is encountered that isn't already in the cache, Wite will re-run the dep bundling process and reload the page.

Monorepos and Linked Dependencies

In a monorepo setup, a dependency may be a linked package from the same repo. Wite automatically detects dependencies that are not resolved from node_modules and treats the linked dep as source code. It will not attempt to bundle the linked dep, and will analyze the linked dep's dependency list instead.

However, this requires the linked dep to be exported as ESM. If not, you can add the dependency to optimizeDeps.include and build.commonjsOptions.include in your config.

export default defineConfig({
  optimizeDeps: {
    include: ['linked-dep']
  },
  build: {
    commonjsOptions: {
      include: [/linked-dep/, /node_modules/]
    }
  }
})

When making changes to the linked dep, restart the dev server with the --force command line option for the changes to take effect.

Deduping

Due to differences in linked dependency resolution, transitive dependencies can deduplicated incorrectly, causing issues when used in runtime. If you stumble on this issue, use npm pack on the linked dependency to fix it.

Customizing the Behavior

The default dependency discovery heuristics may not always be desirable. In cases where you want to explicitly include/exclude dependencies from the list, use the optimizeDeps config options.

A typical use case for optimizeDeps.include or optimizeDeps.exclude is when you have an import that is not directly discoverable in the source code. For example, maybe the import is created as a result of a plugin transform. This means Wite won't be able to discover the import on the initial scan - it can only discover it after the file is requested by the browser and transformed. This will cause the server to immediately re-bundle after server start.

Both include and exclude can be used to deal with this. If the dependency is large (with many internal modules) or is CommonJS, then you should include it; If the dependency is small and is already valid ESM, you can exclude it and let the browser load it directly.

Caching

File System Cache

Wite caches the pre-bundled dependencies in node_modules/.wite. It determines whether it needs to re-run the pre-bundling step based on a few sources:

  • The dependencies list in your package.json.
  • Package manager lockfiles, e.g. package-lock.json, yarn.lock, or pnpm-lock.yaml.
  • Relevant fields in your wite.config.js, if present.

The pre-bundling step will only need to be re-run when one of the above has changed.

If for some reason you want to force Wite to re-bundle deps, you can either start the dev server with the --force command line option, or manually delete the node_modules/.wite cache directory.

Browser Cache

Resolved dependency requests are strongly cached with HTTP headers max-age=31536000,immutable to improve page reload performance during dev. Once cached, these requests will never hit the dev server again. They are auto invalidated by the appended version query if a different version is installed (as reflected in your package manager lockfile). If you want to debug your dependencies by making local edits, you can:

  1. Temporarily disable cache via the Network tab of your browser devtools;
  2. Restart Wite dev server with the --force flag to re-bundle the deps;
  3. Reload the page.

Released under the MIT License.