Skip to main content

Internal Architecture

This document describes the internal architecture and design decisions of the QuickJS-ng JavaScript engine.

Compiler Architecture

Direct Bytecode Generation

The QuickJS compiler generates bytecode directly with no intermediate representation such as a parse tree. This design provides several advantages:
  • Very fast compilation
  • Lower memory usage during compilation
  • Simpler compiler implementation
Several optimization passes are performed over the generated bytecode to improve runtime performance.

Stack-Based Bytecode

QuickJS uses a stack-based bytecode format because it:
  • Is simple to implement and understand
  • Generates compact code
  • Maps well to common CPU architectures
For each function, the maximum stack size is computed at compile time, eliminating the need for runtime stack overflow checks.

Debug Information

A separate compressed line number table is maintained for debug information, keeping bytecode compact while preserving debugging capabilities.

Optimizations

  • Closure variables are optimized and accessed almost as fast as local variables
  • Direct eval in strict mode is specially optimized
  • Multiple bytecode optimization passes improve performance

Runtime Architecture

JSValue Representation

JSValue is the core type representing any JavaScript value (primitives or objects).

32-bit Architecture

  • Uses NaN boxing to store 64-bit floating-point numbers efficiently
  • Representation is optimized for fast testing of 32-bit integers and reference-counted values
  • JSValue fits in two CPU registers for efficient function returns

64-bit Architecture

  • JSValue is 128-bit (no NaN boxing)
  • Rationale: Memory usage is less critical on 64-bit systems
  • Still fits in two CPU registers for efficient returns

Strings

Strings are stored as either:
  • 8-bit character array (for ASCII/Latin-1)
  • 16-bit character array (for Unicode)
Benefits:
  • Random character access is always fast (O(1))
  • Memory efficient for ASCII-heavy content
  • Full Unicode support when needed

C API Integration

The C API provides functions to convert JavaScript strings to UTF-8 encoded C strings. The most common case (ASCII-only strings) involves no copying, improving performance.

Objects

Shape Sharing

Object shapes (also called hidden classes) are shared between objects. A shape includes:
  • Object prototype
  • Property names
  • Property flags
Shapes are shared to save memory and enable optimizations.

Array Optimizations

  • Arrays with no holes (except at the end) are optimized
  • Dense arrays use efficient storage
  • TypedArray accesses are specially optimized

Atoms

Object property names and frequently used strings are stored as Atoms (unique interned strings). Representation:
  • Atoms are 32-bit integers
  • Fast comparison (integer comparison instead of string comparison)
  • Memory savings through deduplication
Special Range: Half of the atom range is reserved for immediate integer literals from 0 to 2³¹-1, allowing common integers to be used as property names without allocation.

Numbers

Numbers are represented as either:
  • 32-bit signed integers (fast path)
  • 64-bit IEEE-754 floating-point values (fallback)
Most arithmetic operations have fast paths for 32-bit integers, significantly improving performance for integer-heavy code.

Garbage Collection

Reference Counting

QuickJS uses reference counting as the primary garbage collection mechanism:
  • Objects are freed automatically and deterministically
  • No pause times for garbage collection
  • Predictable memory usage

Cycle Collection

A separate cycle removal pass runs when allocated memory exceeds a threshold:
  • Detects and breaks reference cycles
  • Only uses reference counts and object content
  • No explicit GC roots need to be manipulated in C code
This hybrid approach provides deterministic cleanup while handling cycles correctly.

Function Calls

The engine is optimized for fast function calls:
  • The system stack holds JavaScript parameters and local variables
  • No separate JavaScript call stack is maintained
  • Direct mapping to native calling conventions
  • Minimal overhead for function calls

Regular Expressions

Custom RegExp Engine

QuickJS includes a custom regular expression engine that is:
  • Small (~15 KiB x86 code, excluding Unicode library)
  • Efficient with good performance characteristics
  • Feature-complete supporting all ES2020+ features including Unicode properties

Direct Bytecode Generation

Like the JavaScript compiler, the RegExp engine:
  • Directly generates bytecode
  • No parse tree is created
  • Fast compilation

Backtracking Implementation

  • Uses explicit stack for backtracking
  • No recursion on the system stack (prevents stack overflow)
  • Simple quantifiers are specially optimized to avoid recursion
  • Infinite recursions from quantifiers with empty terms are prevented

Unicode Support

Custom Unicode Library

QuickJS includes a custom Unicode library to avoid dependency on large external libraries like ICU. Size: ~45 KiB (x86 code) Features:
  • Case conversion
  • Unicode normalization
  • Unicode script queries
  • Unicode general category queries
  • All Unicode binary properties
Design:
  • All Unicode tables are compressed
  • Reasonable access speed maintained
  • Much smaller than alternatives

BigInt Support

libbf Library

BigInt is implemented using the libbf library by Fabrice Bellard. Size: ~90 KiB (x86 code) Features:
  • Arbitrary precision integer arithmetic
  • IEEE 754 floating-point operations with arbitrary precision
  • Transcendental functions with exact rounding
  • Full BigInt specification support

Code Organization

Core Files

  • quickjs.c - Main engine implementation
  • quickjs.h - Public C API
  • quickjs-libc.c - Standard library (std, os modules)
  • quickjs-libc.h - Standard library API
  • libbf.c - BigInt/BigFloat library
  • libregexp.c - Regular expression engine
  • libunicode.c - Unicode support library

Tools

  • qjs.c - Command-line interpreter
  • qjsc.c - JavaScript-to-C compiler
  • run-test262.c - test262 test runner
  • api-test.c - C API test suite

Build System

  • CMakeLists.txt - Primary build system
  • Makefile - Helper Makefile (wraps CMake)
  • meson.build - Alternative Meson build support

Generated Code

  • gen/repl.c - REPL implementation (generated from repl.js)
  • gen/standalone.c - Standalone runtime (generated from standalone.js)
  • Various built-in modules (generated from JS sources)

Build Configuration

CMake Options

  • CMAKE_BUILD_TYPE - Debug, Release, RelWithDebInfo, MinSizeRel
  • BUILD_SHARED_LIBS - Build as shared library
  • QJS_BUILD_EXAMPLES - Build example programs
  • QJS_BUILD_LIBC - Include std/os modules
  • QJS_ENABLE_ASAN - Enable AddressSanitizer
  • QJS_ENABLE_UBSAN - Enable UndefinedBehaviorSanitizer
  • QJS_ENABLE_MSAN - Enable MemorySanitizer
  • QJS_BUILD_WERROR - Treat warnings as errors
  • QJS_DISABLE_PARSER - Build without parser (bytecode-only)
  • QJS_WASI_REACTOR - Build as WASI reactor module

Feature Flags

  • CONFIG_BIGNUM - Enable BigInt/BigDecimal support
  • JS_NAN_BOXING - Enable NaN boxing (32-bit only)
  • JS_CHECK_JSVALUE - Enable JSValue consistency checks
  • CONFIG_ATOMICS - Enable SharedArrayBuffer and Atomics

Memory Management

Allocation Strategy

  • Reference counting for deterministic cleanup
  • Cycle detection for circular references
  • Custom allocator support via JS_NewRuntime2()
  • Optional mimalloc integration for improved performance

Memory Limits

  • Configurable memory limits per runtime
  • Stack size limits checked at compile time
  • Malloc limits to prevent OOM

Performance Characteristics

Strengths

  • Fast compilation (direct bytecode generation)
  • Fast function calls (system stack usage)
  • Efficient integer operations (32-bit fast path)
  • Low memory overhead (shape sharing, atoms, reference counting)
  • Predictable memory usage (deterministic GC)

Trade-offs

  • Reference counting overhead on assignments
  • Cycle detection pass when memory grows
  • 128-bit JSValue on 64-bit platforms uses more memory than NaN boxing

Design Philosophy

QuickJS-ng follows these principles:
  1. Simplicity - Avoid complex intermediate representations
  2. Performance - Optimize common cases (integers, function calls)
  3. Compactness - Minimize code size and memory usage
  4. Compliance - Support the full ECMAScript specification
  5. Embeddability - Easy to integrate into other applications
  6. Portability - Work across platforms and architectures

Build docs developers (and LLMs) love