Skip to main content
Oxc’s architecture is built on four fundamental principles that work together to deliver exceptional performance while maintaining correctness and developer experience.

The Four Pillars

1. Zero-Copy Architecture

The zero-copy principle is the most critical aspect of Oxc’s performance. By minimizing data copying and allocation overhead, Oxc achieves performance that is often 10-100x faster than comparable tools.

Arena-Based Allocation

At the heart of Oxc’s zero-copy architecture is the arena allocator (oxc_allocator). All AST nodes for a compilation unit are allocated in a single memory arena.

How It Works

// Example: Creating AST nodes with arena allocation
use oxc_allocator::Allocator;
use oxc_parser::Parser;

let allocator = Allocator::default();
let source_text = "const x = 1;";
let parser_return = Parser::new(&allocator, source_text, source_type).parse();

// All AST nodes are in the arena - no individual allocations
// No reference counting overhead
// When allocator is dropped, all memory is freed at once

Benefits

No Reference Counting

Eliminates Rc/Arc overhead and atomic operations from hot paths

Cache Locality

Contiguous allocation improves CPU cache utilization

Fast Deallocation

Entire arena dropped at once - no individual node deallocation

Structural Sharing

Enables efficient sharing of AST subtrees

String Optimization

Short strings are inlined using CompactString:
  • Strings ≤ 24 bytes on 64-bit systems are stored inline
  • No heap allocation for small strings
  • Transparent upgrade to heap allocation when needed
Long strings are allocated in the arena:
  • Single allocation per string
  • No copying during AST operations
  • Lifetime tied to arena

Lifetime Management

// All AST nodes have lifetime 'a tied to the allocator
struct Program<'a> {
    body: Vec<'a, Statement<'a>>,
    // ... other fields
}

// Ensures AST cannot outlive the allocator
// Compiler enforces memory safety at compile time
While this requires explicit lifetime annotations, it provides compile-time guarantees of memory safety without runtime overhead.

Borrowed References in Hot Paths

Visitors operate on borrowed references (&Node<'a> or &mut Node<'a>), never owned values:
  • No allocation during traversal
  • No cloning of nodes
  • Zero-copy reads and modifications

Performance Impact

Measured improvements from arena allocation:
  • 10-50% faster parsing compared to Rc/Arc-based approaches
  • Predictable memory usage - single large allocation vs many small ones
  • Faster garbage collection - single drop instead of thousands
The arena allocator trades flexibility for performance. AST nodes cannot be easily moved between arenas or outlive their allocator.

2. Visitor Pattern

Oxc uses the visitor pattern for AST traversal, implemented through automatically generated traits via procedural macros.

Generated Visitor Traits

Oxc provides two visitor traits:
  • Visit - Immutable traversal (read-only analysis)
  • VisitMut - Mutable traversal (AST transformation)
These traits are automatically generated from AST definitions using procedural macros (oxc_ast_macros).

How It Works

use oxc_ast_visit::Visit;

// Implement only the methods you need
struct MyVisitor;

impl<'a> Visit<'a> for MyVisitor {
    fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) {
        // Custom logic for identifier references
        println!("Found identifier: {}", ident.name);
    }
    
    fn visit_function_declaration(&mut self, func: &Function<'a>) {
        // Custom logic for functions
        println!("Found function: {:?}", func.id);
        
        // Continue traversal
        walk_function(self, func);
    }
}

Visitor Generation Process

Benefits

Type Safety

Compiler enforces correct usage - impossible to miss fields or use wrong types

Zero Boilerplate

Only implement methods for nodes you care about

Efficient Dispatch

Static dispatch - no virtual function overhead

Consistent Traversal

All tools traverse AST in same order

Traversal Order

Visitors traverse AST fields in the same order they are defined in the AST types, which follows the “Evaluation order” from the ECMAScript specification.
Example for VariableDeclaration:
  1. Visit kind (const/let/var)
  2. Visit declarations (array of declarators)
  3. For each declarator: pattern → initializer
This ensures consistent behavior across all tools and matches developer expectations.

Alternative: Traverse

For more complex transformations, use oxc_traverse:
  • Provides Traverse trait with enter/exit hooks
  • Access to traversal context
  • Ability to insert/remove nodes during traversal
  • Scope-aware transformations
use oxc_traverse::Traverse;

impl<'a> Traverse<'a> for MyTransform {
    fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
        // Transform on entry
    }
    
    fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
        // Transform after children processed
    }
}

Visitor Use Cases

ToolVisitor TypePurpose
LinterVisitRead-only analysis to detect issues
SemanticVisitBuild symbol tables and scopes
TransformerTraverseModify AST with scope awareness
MinifierTraverseOptimize and transform code
CodegenManualGenerate code (no visitor needed)

3. Shared Infrastructure

Common functionality is shared across all components to ensure consistency and avoid duplication.

Shared Foundation Crates

oxc_diagnostics: Unified Error Reporting

Purpose: Consistent error messages across all tools Features:
  • Source code snippets with highlighting
  • Multiple output formats (human-readable, JSON)
  • Severity levels (error, warning, info)
  • Related information and suggestions
  • LSP-compatible diagnostics
Example Output:
  × Unexpected token
   ╭─[test.js:1:10]
 1 │ const x = ;
   ·          ─
   ╰────
  help: Expected an expression

oxc_span: Unified Position Tracking

Purpose: Consistent source position representation Key Decisions:
  • Uses u32 instead of usize for memory efficiency
  • Byte-based offsets (not character-based)
  • Supports UTF-8 correctly
Structure:
struct Span {
    start: u32,  // Byte offset from start of file
    end: u32,    // Byte offset from start of file
}
Benefits:
  • Compact representation (8 bytes on 64-bit systems)
  • Fast comparison and operations
  • Consistent across all components
  • Enables efficient source map generation

oxc_syntax: Language Definitions

Purpose: Shared syntax knowledge Contents:
  • Token definitions
  • Keyword mappings
  • Operator precedence
  • Node, reference, scope, and symbol flags
  • Language feature flags
Why Shared?
  • Ensures parser and semantic analyzer agree on syntax
  • Single source of truth for language features
  • Easier to add new language features

Benefits of Sharing

Consistency

All tools report errors in the same format and use the same definitions

Maintainability

Changes to shared infrastructure benefit all tools

Smaller Binary

Code sharing reduces final binary size

Correctness

Single source of truth reduces bugs from inconsistencies

4. Modular Composability

Oxc is not a monolithic tool - it’s a collection of independent, reusable components that can be composed in different ways.

Independent Crates

Each major component is published as an independent crate:
# Use just the parser
[dependencies]
oxc_parser = "0.x"
oxc_allocator = "0.x"
oxc_ast = "0.x"

# Or use the full toolchain
[dependencies]
oxc = { version = "0.x", features = ["full"] }

Composition Patterns

Pattern 1: Parse Only

use oxc_allocator::Allocator;
use oxc_parser::Parser;

let allocator = Allocator::default();
let result = Parser::new(&allocator, source_text, source_type).parse();
let program = result.program;
// Use AST for analysis

Pattern 2: Parse + Semantic

use oxc_semantic::SemanticBuilder;

let parser_return = Parser::new(&allocator, source_text, source_type).parse();
let semantic = SemanticBuilder::new()
    .with_cfg(true)
    .build(&parser_return.program)
    .semantic;
// Access symbol tables and scopes

Pattern 3: Full Pipeline

use oxc_transformer::Transformer;
use oxc_codegen::Codegen;

// Parse
let parser_return = Parser::new(&allocator, source_text, source_type).parse();

// Semantic analysis
let semantic = SemanticBuilder::new().build(&parser_return.program).semantic;

// Transform
let mut program = parser_return.program;
let transform_result = Transformer::new(&allocator, transform_options)
    .build_with_scoping(semantic.scoping, &mut program);

// Generate code
let result = Codegen::new().build(&program);
let output_code = result.code;

Use Cases

Need: Just parse and analyze ASTComponents: oxc_parser + oxc_allocator + oxc_astExample: ESLint plugin that analyzes code structure

Cargo Features

Components expose cargo features for fine-grained control:
[dependencies]
oxc_semantic = { version = "0.x", features = ["cfg", "jsdoc"] }
oxc_ast = { version = "0.x", features = ["serialize"] }
Common features:
  • serialize - Enable ESTree JSON serialization
  • cfg - Include control flow graph
  • jsdoc - Parse JSDoc comments
  • linter - Enable linter-specific optimizations

Benefits

Pay for What You Use

Only include components you need - smaller binaries

Flexibility

Compose tools in novel ways for custom use cases

Independent Evolution

Components can evolve independently with semantic versioning

Easy Integration

Drop individual components into existing projects

Principles in Action: Linter Example

Let’s see how all four principles work together in the linter: Step by Step:
  1. Zero-Copy: Parse file into arena-allocated AST
  2. Shared Infrastructure: Use oxc_span for positions, oxc_diagnostics for errors
  3. Modular: Parser and semantic analyzer are independent crates
  4. Visitor: Each lint rule implements visitor pattern
  5. Zero-Copy: Rules operate on borrowed AST references
  6. Shared Infrastructure: All rules produce consistent diagnostics

Design Trade-offs

These principles involve trade-offs. Oxc optimizes for performance and correctness at the cost of some development complexity.

Arena Allocation

BenefitTrade-off
10-50% faster performanceRequires lifetime annotations
Simpler memory managementLess flexible memory patterns
Better cache localityCannot easily share nodes between arenas

Visitor Pattern with Macros

BenefitTrade-off
Type-safe traversalLonger compile times (proc macros)
Zero boilerplateLearning curve for contributors
Compile-time guaranteesMore complex error messages

Shared Infrastructure

BenefitTrade-off
Consistency across toolsChanges affect all components
Reduced duplicationMore careful API design required
Smaller binariesTighter coupling between crates

Modular Composability

BenefitTrade-off
Flexible compositionMore crates to manage
Smaller dependenciesVersion compatibility complexity
Independent evolutionNeed to maintain stable APIs

Architecture Overview

Comprehensive overview of Oxc’s architecture and components

Performance

Deep dive into performance implementation and benchmarks

Build docs developers (and LLMs) love