TL;DR
acorn is the foundational ECMAScript parser — small, fast, spec-compliant ESTree output, and the parser that espree and many other tools build on. @babel/parser is Babel's parser — supports TypeScript, JSX, Flow, and experimental proposals through plugins, and produces Babel-specific AST (a superset of ESTree). espree is ESLint's parser — wraps acorn, ensures compatibility with ESLint's AST expectations. In 2026: use acorn for plain JavaScript parsing with minimal overhead, @babel/parser when you need TypeScript or JSX support, and espree when you're building ESLint rules or plugins.
Key Takeaways
- acorn: ~20M weekly downloads — tiny (100KB), ESTree-compliant, powers espree and rollup
- @babel/parser: ~15M weekly downloads — TypeScript + JSX + experimental proposals, Babel AST
- espree: ~20M weekly downloads — ESLint's parser, wraps acorn, ESTree-compliant
- ESTree is the standard AST spec — most tools expect ESTree-compatible output
- @babel/parser AST differs from ESTree in some nodes (use
@babel/typeshelpers) - For TypeScript:
@babel/parser(no type checking) ortypescriptcompiler API (type-aware)
What is an AST?
// Source code: "const x = 1 + 2"
// AST (Abstract Syntax Tree):
{
"type": "Program",
"body": [{
"type": "VariableDeclaration",
"kind": "const",
"declarations": [{
"type": "VariableDeclarator",
"id": { "type": "Identifier", "name": "x" },
"init": {
"type": "BinaryExpression",
"operator": "+",
"left": { "type": "Literal", "value": 1 },
"right": { "type": "Literal", "value": 2 }
}
}]
}]
}
// Uses: linters, formatters, bundlers, codemods, transpilers,
// tree-shaking, dead code elimination, code generation
acorn
acorn — the tiny, fast JavaScript parser:
Basic parsing
import * as acorn from "acorn"
// Parse JavaScript:
const ast = acorn.parse(`const x = 1 + 2`, {
ecmaVersion: 2022, // Supported ECMAScript version
sourceType: "module", // "module" or "script"
locations: true, // Include line/column info in nodes
ranges: true, // Include start/end character positions
})
console.log(ast.type) // "Program"
console.log(ast.body[0].type) // "VariableDeclaration"
// Walk the AST:
import { simple as simpleWalk } from "acorn-walk"
simpleWalk(ast, {
Identifier(node) {
console.log("Found identifier:", node.name)
},
Literal(node) {
console.log("Found literal:", node.value)
},
})
Plugins (acorn-jsx, import assertions)
import * as acorn from "acorn"
import jsx from "acorn-jsx"
// Add JSX support via plugin:
const jsxParser = acorn.Parser.extend(jsx())
const jsxAst = jsxParser.parse(`const el = <div className="app">Hello</div>`, {
ecmaVersion: 2022,
sourceType: "module",
})
// acorn-import-attributes for import assertions:
import importAttributes from "acorn-import-attributes"
const extendedParser = acorn.Parser.extend(importAttributes)
const withImport = extendedParser.parse(
`import data from "./data.json" with { type: "json" }`,
{ ecmaVersion: 2022, sourceType: "module" }
)
Used by: rollup, webpack, vite (under the hood)
acorn is the parser for:
- Rollup (bundles using acorn-parsed ASTs)
- webpack (via terser for minification, which uses acorn)
- ESLint (via espree, which wraps acorn)
- Prettier (via @babel/parser, but acorn for some cases)
- Vite (uses magic-string with acorn for HMR transforms)
Characteristics:
- ESTree-compliant (all tools that read ESTree work with acorn output)
- No TypeScript (use @babel/parser or typescript compiler)
- No JSX by default (use acorn-jsx plugin)
- Very fast and small (core is ~80KB)
@babel/parser
@babel/parser — Babel's extensible parser:
Basic parsing with TypeScript
import { parse } from "@babel/parser"
import type { File } from "@babel/types"
// Parse JavaScript:
const jsAst = parse(`const x = 1 + 2`, {
sourceType: "module",
})
// Parse TypeScript:
const tsAst = parse(
`
interface Package {
name: string
version: string
score: number
}
const getPackage = async (name: string): Promise<Package> => {
return fetch(\`/api/\${name}\`).then(r => r.json())
}
`,
{
sourceType: "module",
plugins: ["typescript"],
}
)
console.log(tsAst.program.body[0].type) // "TSInterfaceDeclaration"
Plugin combinations
import { parse } from "@babel/parser"
// TypeScript + JSX (for .tsx files):
const tsxAst = parse(
`
interface Props { name: string }
const Greeting: React.FC<Props> = ({ name }) => (
<div className="greeting">Hello, {name}!</div>
)
`,
{
sourceType: "module",
plugins: [
"typescript",
"jsx",
],
}
)
// Experimental proposals:
const decoratorsAst = parse(
`
@Injectable()
class UserService {
@Inject(DB)
private db: Database
}
`,
{
sourceType: "module",
plugins: [
"typescript",
["decorators", { decoratorsBeforeExport: true }],
"classProperties",
],
}
)
AST traversal with @babel/traverse
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
import * as t from "@babel/types"
const code = `
const greet = (name) => {
console.log("Hello, " + name)
}
`
const ast = parse(code, { sourceType: "module" })
// Traverse and transform the AST:
traverse(ast, {
// Called for every arrow function:
ArrowFunctionExpression(path) {
// Convert arrow functions to regular functions:
path.replaceWith(
t.functionExpression(
null,
path.node.params,
path.node.body as t.BlockStatement
)
)
},
// Called for every string concatenation:
BinaryExpression(path) {
if (path.node.operator === "+" &&
t.isStringLiteral(path.node.left)) {
// Convert: "Hello, " + name → `Hello, ${name}`
path.replaceWith(
t.templateLiteral(
[t.templateElement({ raw: "Hello, " }), t.templateElement({ raw: "" })],
[path.node.right]
)
)
}
},
})
// Generate code back from modified AST:
const { code: transformed } = generate(ast)
console.log(transformed)
// const greet = function(name) { console.log(`Hello, ${name}`) }
Scope analysis
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
const code = `
import { useState } from "react"
const [count, setCount] = useState(0)
const doubled = count * 2
`
const ast = parse(code, {
sourceType: "module",
plugins: ["typescript"],
})
traverse(ast, {
ImportDeclaration(path) {
console.log(`Import from: ${path.node.source.value}`)
// "react"
for (const spec of path.node.specifiers) {
console.log(` → ${spec.local.name}`)
}
},
// Scope tracking:
Identifier(path) {
if (path.scope.hasBinding(path.node.name)) {
const binding = path.scope.getBinding(path.node.name)
console.log(`${path.node.name} is defined at line ${binding?.path.node.loc?.start.line}`)
}
},
})
espree
espree — ESLint's JavaScript parser:
Used for ESLint rules
import espree from "espree"
// Parse JavaScript (ESTree-compliant output):
const ast = espree.parse(`const x = 1 + 2`, {
ecmaVersion: 2022,
sourceType: "module",
loc: true, // Location info
range: true, // Range info
tokens: true, // Include token array (ESLint uses these)
comment: true, // Include comments
})
// espree AST matches ESTree exactly — compatible with:
// - esquery (CSS-like AST selector engine used by ESLint)
// - esrecurse (recursive AST visitor)
// - ESLint's rule API
Writing an ESLint rule (uses espree under the hood)
// packages/eslint-plugin-pkgpulse/src/rules/no-lodash-clonedeep.ts
import type { Rule } from "eslint"
const rule: Rule.RuleModule = {
meta: {
type: "suggestion",
docs: {
description: "Prefer structuredClone over lodash.cloneDeep",
recommended: true,
},
fixable: "code",
},
create(context) {
return {
// ESLint calls this for every CallExpression node:
CallExpression(node) {
if (
node.callee.type === "MemberExpression" &&
node.callee.object.type === "Identifier" &&
node.callee.object.name === "_" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "cloneDeep"
) {
context.report({
node,
message: "Use structuredClone() instead of _.cloneDeep()",
fix(fixer) {
const arg = node.arguments[0]
const argText = context.getSourceCode().getText(arg)
return fixer.replaceText(node, `structuredClone(${argText})`)
},
})
}
},
}
},
}
export default rule
Feature Comparison
| Feature | acorn | @babel/parser | espree |
|---|---|---|---|
| AST spec | ESTree | Babel (superset) | ESTree |
| TypeScript | ❌ (plugin) | ✅ | ❌ |
| JSX | ✅ (plugin) | ✅ | ✅ (plugin) |
| Decorators | ❌ | ✅ | ❌ |
| Bundle size | ~80KB | ~1MB | ~100KB |
| Token output | ✅ | ✅ | ✅ |
| ESLint compatible | ⚠️ (via espree) | ⚠️ (via @babel/eslint-parser) | ✅ |
| Scope analysis | ❌ (needs eslint-scope) | ✅ @babel/traverse | ✅ ESLint API |
| Weekly downloads | ~20M | ~15M | ~20M |
When to Use Each
Choose acorn if:
- Parsing plain JavaScript (no TypeScript, no JSX)
- Building a bundler, minifier, or low-level tool
- Need the smallest footprint with ESTree-compliant output
- Already using rollup/webpack ecosystem tools
Choose @babel/parser if:
- Need TypeScript, JSX, or Flow parsing
- Writing Babel plugins or transforms
- Building a codemod tool that transforms TypeScript/JSX
- Need scope analysis via
@babel/traverse
Choose espree if:
- Writing ESLint rules or plugins
- Building tools that need to be compatible with ESLint's AST
- Working in the ESLint ecosystem (eslint-plugin-, eslint-config-)
For TypeScript with full type information:
// None of these parsers provide TypeScript type checking
// For type-aware analysis, use the TypeScript compiler API:
import ts from "typescript"
const program = ts.createProgram(["src/index.ts"], {
strict: true,
target: ts.ScriptTarget.ES2022,
})
const typeChecker = program.getTypeChecker()
// Access types, symbols, declarations — full semantic analysis
Building Codemods with AST Parsers
Codemods are automated refactoring scripts that transform source code at scale — they are where AST parsers deliver their most tangible value for application developers. When migrating a large codebase from CommonJS require() to ESM import, or renaming a deprecated API across hundreds of files, manually editing is error-prone. Instead, you parse the file to an AST, traverse it to find matching nodes, mutate the AST, and regenerate code.
@babel/parser is the practical choice for most codemods because it handles TypeScript, JSX, and modern syntax out of the box. Combined with @babel/traverse for AST walking and @babel/generator for code generation, you have a complete pipeline. The jscodeshift library from Facebook/Meta wraps this pipeline and provides a collection-based API modeled after jQuery that makes transforms easier to write and test. For example, a codemod to replace all React.createClass calls with ES class syntax, or to swap moment() for dayjs() across an entire monorepo, typically involves 30-50 lines of jscodeshift code and runs in seconds across thousands of files.
acorn is preferred by lower-level tooling that needs to stay lean. Rollup uses acorn to parse each module's source during bundling; it only needs to understand imports and exports, so the minimal footprint matters. Tools like recast (which preserves formatting through AST transforms) support acorn as a parser option precisely because it produces spec-compliant ESTree output that recast can round-trip reliably.
espree is the right choice when your codemod doubles as an ESLint rule. ESLint rule fixers use the same fixer.replaceText and fixer.insertTextBefore API regardless of parser, but the AST node shapes must match ESTree exactly. Using espree directly in your tool ensures identical node structures to what ESLint sees, which matters when you are building a plugin that both reports problems and auto-fixes them.
Performance Characteristics and Practical Tradeoffs
Parse performance is rarely the bottleneck in interactive tooling, but it matters significantly in CI pipelines that lint thousands of files per run or in bundlers processing large dependency trees on every change.
Acorn is the fastest of the three. Its parser is a hand-tuned recursive descent implementation with no overhead from plugin infrastructure. In benchmarks across representative JavaScript files, acorn typically parses 20-40% faster than @babel/parser on plain JavaScript. This gap closes for files that use many modern syntax features since both parsers spend similar time on complex productions. The bundle size advantage is real: acorn's core is around 80KB compared to @babel/parser's roughly 1MB, which matters for browser-side tools and any environment that pays a parse cost for the parser itself.
@babel/parser incurs overhead from its extensible plugin architecture. Each plugin adds to the parser state and requires checking for plugin presence during parsing. Loading both typescript and jsx plugins adds a measurable but usually acceptable cost. The major practical tradeoff is AST shape: Babel's AST extends ESTree with additional node types (TSInterfaceDeclaration, JSXElement, etc.) and slightly different representations for some common nodes. Code using @babel/traverse and @babel/types helpers is insulated from these differences, but tools that try to read Babel AST nodes as if they were pure ESTree will encounter subtle bugs.
espree wraps acorn and adds token and comment attachment, which ESLint requires. The wrapper overhead is minimal, but espree's output includes tokens and comments arrays that pure acorn does not provide by default. This makes espree output slightly larger in memory, which is irrelevant for single-file operations but accumulates when caching ASTs for an entire project.
Ecosystem Integrations and When Each Parser Gets Pulled In
Understanding which parser is already in your node_modules helps avoid redundant installations and aligns tool choices with existing dependencies. If your project uses ESLint, espree is already present as a transitive dependency — there is no marginal cost to using it directly for custom ESLint rule development. If your project uses Babel for transpilation, @babel/parser is already installed; using it for codemods or custom transforms adds no new dependencies.
Rollup bundles ship with acorn as a direct dependency. If you are using Vite (which is built on Rollup), acorn is also in your tree. Several popular tools including magic-string, astring, and meriyah either use or produce ESTree-compatible ASTs, making them natural companions to acorn-based workflows.
One important ecosystem note: typescript-eslint is the de facto standard for linting TypeScript code in 2026. It ships its own parser (@typescript-eslint/parser) that wraps the TypeScript compiler API and produces an ESTree-compatible AST with TypeScript extensions. This parser provides access to type information during linting — something none of acorn, @babel/parser, or espree can do. If you are choosing between @babel/parser and typescript-eslint/parser for TypeScript-aware tooling, prefer the latter whenever you need semantic type data rather than just syntactic structure.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on acorn v8.x, @babel/parser v7.x, and espree v9.x.
Choosing a JavaScript AST parser in 2026 is largely a matter of matching the tool to the dependency context that already exists in your project. Acorn is the right default for lean, spec-compliant parsing of plain JavaScript — its ESTree output integrates directly with the broad ecosystem of AST utilities, and it is already present as a transitive dependency in most projects using Rollup or Vite. @babel/parser is the choice whenever TypeScript or JSX is involved and you need to transform the resulting AST, because its @babel/traverse and @babel/generator companions complete a full parse-transform-generate pipeline without additional packages. Espree is the correct choice when your goal is building or extending ESLint rules, as it produces the exact AST shape ESLint's rule API expects, including token and comment arrays. The boundaries between these tools are cleaner than they first appear — each has a distinct purpose, and they often complement rather than compete with each other in the same project.
Compare developer tooling and parser packages on PkgPulse →
See also: AVA vs Jest and ohash vs object-hash vs hash-wasm, decimal.js vs big.js vs bignumber.js.