← back to blog
2026-03-25 open-source

Rewriting Import Cost: From Webpack to esbuild, Babel to SWC

Import Cost is a popular VS Code extension that shows the size of imported packages inline in your editor. The original version used Webpack and Babel — heavy tools that worked, but were slow and bloated. I rewrote the core to use modern, Rust/Go-based tooling and the results were dramatic.

The Problem

The original Import Cost had 16 runtime dependencies including Webpack, Babel, cheerio, terser, worker-farm, css-loader, file-loader, url-loader, and 12 browser polyfills. For what’s essentially a “bundle this import and tell me the size” operation, this was overkill.

Every time you typed an import statement, the extension would:

  1. Parse the file with Babel (JavaScript, slow)
  2. Bundle the import with Webpack (JavaScript, slow)
  3. Minify with terser (JavaScript, slow)
  4. Report the size

The whole pipeline could take several seconds for large packages.

The Rewrite

I replaced every JavaScript tool with its Rust or Go equivalent:

BeforeAfterSpeedup
Babel (parser)SWC (Rust)5-10x faster
Webpack (bundler)esbuild (Go)10-100x faster
ESLint + PrettierBiome (Rust)Single tool
Webpack (extension build)esbuildInstant builds

SWC for Parsing

SWC is a Rust-based JavaScript/TypeScript compiler. For parsing import statements, it’s massively faster than Babel:

import { parseFile } from '@swc/core';

async function extractImports(filePath: string) {
  const ast = await parseFile(filePath, {
    syntax: 'typescript',
    tsx: true,
  });

  const imports: string[] = [];

  for (const node of ast.body) {
    if (node.type === 'ImportDeclaration') {
      imports.push(node.source.value);
    }
    if (node.type === 'VariableDeclaration') {
      for (const decl of node.declarations) {
        if (
          decl.init?.type === 'CallExpression' &&
          decl.init.callee.type === 'Identifier' &&
          decl.init.callee.value === 'require'
        ) {
          const arg = decl.init.arguments[0];
          if (arg?.expression.type === 'StringLiteral') {
            imports.push(arg.expression.value);
          }
        }
      }
    }
  }

  return imports;
}

esbuild for Bundling

esbuild replaces both Webpack (bundling) and terser (minification) in a single pass:

import { build } from 'esbuild';
import { writeFileSync, unlinkSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { gzipSync } from 'zlib';

async function calculateImportSize(packageName: string) {
  const entryContent = `import '${packageName}';`;
  const entryFile = join(tmpdir(), `import-cost-${Date.now()}.js`);

  writeFileSync(entryFile, entryContent);

  try {
    const result = await build({
      entryPoints: [entryFile],
      bundle: true,
      write: false,
      minify: true,
      platform: 'browser',
      format: 'esm',
      treeShaking: true,
      metafile: true,
    });

    const output = result.outputFiles[0];
    const minifiedSize = output.contents.byteLength;
    const gzippedSize = gzipSync(output.contents).byteLength;

    return { minifiedSize, gzippedSize };
  } finally {
    unlinkSync(entryFile);
  }
}

This runs in milliseconds where Webpack took seconds. esbuild’s Go implementation handles bundling, tree-shaking, and minification in a single pass.

Project Structure

The project uses npm workspaces as a monorepo:

packages/
├── import-cost/          # Core Node module
│   ├── src/
│   │   ├── parser.ts     # SWC-based import extraction
│   │   ├── bundler.ts    # esbuild-based size calculation
│   │   └── index.ts      # Public API
│   └── package.json
├── vscode-import-cost/   # VS Code extension
│   ├── src/
│   │   ├── extension.ts  # Extension entry point
│   │   └── decorator.ts  # Inline size display
│   └── package.json
├── coc-import-cost/      # Vim/Neovim extension
│   └── ...
└── native-fs-adapter/    # Filesystem cache adapter
    └── ...

The core import-cost package is framework-agnostic — it exports a simple API that any editor extension can use:

import { importCost } from 'import-cost';

const results = await importCost('/path/to/file.ts');
// [{ name: 'lodash', size: 72384, gzip: 25412, line: 1 }]

Results

After the rewrite:

  • Runtime dependencies: 16 → 2 (esbuild, @swc/core)
  • Extension startup: ~3s → ~200ms
  • Import size calculation: 2-5s → 50-200ms
  • Extension bundle size: Significantly smaller

The key takeaway: modern Rust/Go tooling isn’t just faster — it’s simpler. One tool replaces five, and the code is more readable because there are fewer layers of configuration.

Check out the project on GitHub.