Performance Tuning and Optimization
QuickJS is designed to be small and efficient. This guide covers techniques to optimize performance for your specific use case.
Build Options
Compiler Optimizations
From CMakeLists.txt, QuickJS supports various build configurations:
# Release build (optimized)
cmake -DCMAKE_BUILD_TYPE=Release ..
make
# Debug build (development)
cmake -DCMAKE_BUILD_TYPE=Debug ..
make
Release builds use -O2 or -O3 optimizations and strip debug symbols.
Disable Parser
For embedded systems where code is pre-compiled, disable the parser:
cmake -DQJS_DISABLE_PARSER=ON ..
make
This significantly reduces binary size but requires all code to be compiled to bytecode using qjsc.
Static Builds
Create standalone executables with no runtime dependencies:
cmake -DQJS_BUILD_CLI_STATIC=ON ..
make
Sanitizers (Development Only)
Enable sanitizers to catch bugs that may affect performance:
# Address Sanitizer
cmake -DQJS_ENABLE_ASAN=ON ..
# Undefined Behavior Sanitizer
cmake -DQJS_ENABLE_UBSAN=ON ..
# Thread Sanitizer (for workers)
cmake -DQJS_ENABLE_TSAN=ON ..
Never use sanitizers in production - they significantly reduce performance.
Memory Optimization
Tune GC Threshold
Balance memory usage vs. GC frequency:
// Lower threshold = more frequent GC = less memory, more CPU
JS_SetGCThreshold(rt, 512 * 1024); // 512 KB
// Higher threshold = less frequent GC = more memory, less CPU
JS_SetGCThreshold(rt, 8 * 1024 * 1024); // 8 MB
Profile your application to find the optimal threshold.
Set Memory Limits
Prevent memory bloat:
// Hard limit on total memory
JS_SetMemoryLimit(rt, 128 * 1024 * 1024); // 128 MB
Minimize Context Count
Each context has overhead. Share contexts when possible:
// Good: One context for related operations
JSContext *ctx = JS_NewContext(rt);
// Bad: Creating many contexts unnecessarily
for (int i = 0; i < 100; i++) {
JSContext *ctx = JS_NewContext(rt); // Wasteful
// ...
JS_FreeContext(ctx);
}
Use Bytecode Compilation
Pre-compile scripts to bytecode:
# Compile script to bytecode
qjsc -o script.c -m script.js
# Compile to standalone executable
qjsc -o script script.js
Load bytecode instead of source:
// Compile once (at build time or first run)
JSValue bytecode = JS_Eval(ctx, code, len, "file.js",
JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_COMPILE_ONLY);
// Serialize bytecode
size_t bc_len;
uint8_t *bc = JS_WriteObject(ctx, &bc_len, bytecode, JS_WRITE_OBJ_BYTECODE);
// Later: deserialize and execute (much faster)
JSValue bc_obj = JS_ReadObject(ctx, bc, bc_len, JS_READ_OBJ_BYTECODE);
JSValue result = JS_EvalFunction(ctx, bc_obj);
Signatures:
JS_EXTERN uint8_t *JS_WriteObject(JSContext *ctx, size_t *psize,
JSValueConst obj, int flags);
JS_EXTERN JSValue JS_ReadObject(JSContext *ctx, const uint8_t *buf,
size_t buf_len, int flags);
JS_EXTERN JSValue JS_EvalFunction(JSContext *ctx, JSValue fun_obj);
Execution Optimization
Stack Size Configuration
Adjust stack size based on your recursion needs:
// Smaller stack for simple scripts (saves memory)
JS_SetMaxStackSize(rt, 256 * 1024); // 256 KB
// Larger stack for deep recursion
JS_SetMaxStackSize(rt, 2 * 1024 * 1024); // 2 MB
Default is JS_DEFAULT_STACK_SIZE (1 MB).
Minimize JSValue Conversions
Conversions between C and JavaScript types have overhead:
// Inefficient: multiple conversions
for (int i = 0; i < 1000; i++) {
JSValue val = JS_NewInt32(ctx, i);
JS_SetPropertyUint32(ctx, array, i, val);
}
// Better: batch operations when possible
JSValue *values = malloc(1000 * sizeof(JSValue));
for (int i = 0; i < 1000; i++) {
values[i] = JS_NewInt32(ctx, i);
}
JSValue array = JS_NewArrayFrom(ctx, 1000, values);
free(values);
Avoid Unnecessary Exception Checks
Exception checks have minimal overhead, but can add up:
// If you know the operation won't fail, you can skip the check
JSValue val = JS_NewInt32(ctx, 42); // Never fails
// No need to check JS_IsException(val)
// But always check user input
JSValue result = JS_Eval(ctx, user_code, len, "<user>", JS_EVAL_TYPE_GLOBAL);
if (JS_IsException(result)) {
// Handle error
}
Function Call Optimization
Direct C Function Calls
Use C functions instead of JavaScript for performance-critical code:
// Fast: Pure C implementation
static JSValue js_fast_compute(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
int32_t a, b;
JS_ToInt32(ctx, &a, argv[0]);
JS_ToInt32(ctx, &b, argv[1]);
// Complex computation in C
int32_t result = /* ... */;
return JS_NewInt32(ctx, result);
}
Function Magic Numbers
Use magic numbers to create specialized versions:
static JSValue js_array_method(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv, int magic)
{
switch (magic) {
case 0: // push
// ...
break;
case 1: // pop
// ...
break;
case 2: // shift
// ...
break;
}
}
JSCFunctionListEntry funcs[] = {
JS_CFUNC_MAGIC_DEF("push", 1, js_array_method, 0),
JS_CFUNC_MAGIC_DEF("pop", 0, js_array_method, 1),
JS_CFUNC_MAGIC_DEF("shift", 0, js_array_method, 2),
};
ArrayBuffer and Typed Arrays
Zero-Copy Buffers
Share memory between C and JavaScript:
// Allocate buffer outside QuickJS
uint8_t *buffer = malloc(1024 * 1024); // 1 MB
// Wrap in ArrayBuffer (QuickJS takes ownership)
JSValue ab = JS_NewArrayBuffer(ctx, buffer, 1024 * 1024,
free, // free_func
NULL, // opaque
false); // is_shared
// JavaScript can now access buffer with zero copy
Signature:
typedef void JSFreeArrayBufferDataFunc(JSRuntime *rt, void *opaque, void *ptr);
JS_EXTERN JSValue JS_NewArrayBuffer(JSContext *ctx, uint8_t *buf, size_t len,
JSFreeArrayBufferDataFunc *free_func,
void *opaque, bool is_shared);
Direct Buffer Access
Access buffers without copying:
size_t size;
uint8_t *data = JS_GetArrayBuffer(ctx, &size, arraybuffer_val);
if (data) {
// Process data directly
for (size_t i = 0; i < size; i++) {
data[i] = /* ... */;
}
}
Signature:
JS_EXTERN uint8_t *JS_GetArrayBuffer(JSContext *ctx, size_t *psize,
JSValueConst obj);
Interrupt Handler
Timeout Implementation
typedef struct {
int64_t start_time;
int64_t timeout_ms;
} TimeoutData;
static int64_t get_time_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static int interrupt_handler(JSRuntime *rt, void *opaque)
{
TimeoutData *data = opaque;
int64_t now = get_time_ms();
// Return non-zero to interrupt execution
return (now - data->start_time) > data->timeout_ms;
}
TimeoutData timeout_data = {
.start_time = get_time_ms(),
.timeout_ms = 5000, // 5 second timeout
};
JS_SetInterruptHandler(rt, interrupt_handler, &timeout_data);
Signature:
typedef int JSInterruptHandler(JSRuntime *rt, void *opaque);
JS_EXTERN void JS_SetInterruptHandler(JSRuntime *rt,
JSInterruptHandler *cb, void *opaque);
The interrupt handler is called periodically during execution. Keep it fast!
Module Loading
Optimize Module Resolution
Cache module loading to avoid repeated file I/O:
typedef struct {
// Cache of loaded modules
// module_name -> JSModuleDef
} ModuleCache;
static JSModuleDef *cached_module_loader(JSContext *ctx,
const char *module_name,
void *opaque,
JSValueConst attributes)
{
ModuleCache *cache = opaque;
// Check cache first
JSModuleDef *m = cache_lookup(cache, module_name);
if (m)
return m;
// Load and cache
m = load_module(ctx, module_name);
cache_store(cache, module_name, m);
return m;
}
Profiling
Enable Bytecode Dumps
// Dump bytecode to understand execution
JS_SetDumpFlags(rt, JS_DUMP_BYTECODE_FINAL);
Dump flags:
#define JS_DUMP_BYTECODE_FINAL 0x01 // final bytecode
#define JS_DUMP_BYTECODE_STEP 0x80 // executed bytecode
#define JS_DUMP_GC 0x400 // GC events
Measure Execution Time
#include <time.h>
static int64_t get_time_us(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
}
int64_t start = get_time_us();
JSValue result = JS_Eval(ctx, code, len, "bench.js", JS_EVAL_TYPE_GLOBAL);
int64_t elapsed = get_time_us() - start;
printf("Execution time: %lld μs\n", elapsed);
JavaScript-Level Optimizations
While this guide focuses on C API, remember these JavaScript best practices:
- Avoid
eval() and new Function() - they prevent JIT optimization
- Use typed arrays for numeric data instead of regular arrays
- Minimize property access in tight loops
- Prefer
const and let over var
- Use strict mode (
"use strict")
Benchmarking
Example benchmark harness:
#include <quickjs.h>
#include <time.h>
int main(void) {
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
const char *code = "/* your code */";
// Warmup
for (int i = 0; i < 10; i++) {
JSValue v = JS_Eval(ctx, code, strlen(code), "<bench>", JS_EVAL_TYPE_GLOBAL);
JS_FreeValue(ctx, v);
}
// Benchmark
int64_t start = get_time_us();
const int iterations = 1000;
for (int i = 0; i < iterations; i++) {
JSValue v = JS_Eval(ctx, code, strlen(code), "<bench>", JS_EVAL_TYPE_GLOBAL);
JS_FreeValue(ctx, v);
}
int64_t elapsed = get_time_us() - start;
printf("Average: %.2f μs/iteration\n", (double)elapsed / iterations);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
Next Steps