Boosting Node.js Performance: AsyncLocalStorage and tegg v3 Deep Dive
This article explains how AsyncLocalStorage works in Node.js, demonstrates its usage with eggjs, presents benchmark results comparing tegg v3 to earlier versions, analyzes performance bottlenecks, and shows code transformations that reduce memory and CPU overhead for large-scale applications.
AsyncLocalStorage and eggjs
AsyncLocalStorage[1] can safely retrieve variables stored in a context across an async function and its related asynchronous operations. The following code demonstrates its behavior:
<code>const http = require('http');
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
function logWithId(msg) {
const id = asyncLocalStorage.getStore();
console.log(`${id}: `, msg);
}
let idSeq = 0;
http.createServer((req, res) => {
asyncLocalStorage.run(idSeq++, () => {
logWithId('start');
setImmediate(() => {
logWithId('finish');
res.end();
});
});
}).listen(8080);
</code>Running two HTTP requests (e.g., with
curl http://127.0.0.1:8080) prints:
<code>0: start
0: finish
1: start
1: finish
</code>Node.js uses V8’s Promise lifecycle hooks[2][3] to track Promise creation, resolution, rejection, and chaining. AsyncLocalStorage propagates the async ID via these hooks, allowing a storage object to retrieve the current context safely. Besides Promise, modules such as timer, fs, and net also propagate context through the
AsyncResourceabstraction[4].
Both Koa[5] and Egg[6] have added support: Koa enables it via the
asyncLocalStorageoption, while Egg exposes the current context through
app.currentContext.
Benchmark
Test repository: https://github.com/eggjs/tegg_benchmark/actions/runs/4025979558
Project Scale Test
Simulates large projects by creating many controllers and services, testing 1, 10, 100, 1,000, and 10,000 components.
Business Complexity Test
Simulates increasing business complexity by having controllers call services, testing the same component counts.
Conclusion
tegg v3 does not suffer performance degradation as project size grows.
Performance loss caused by business complexity growth is slower in tegg v3 than in egg/tegg v1.
Profile – CPU
Hotspots concentrate on Egg’s
definePropertyand
ClassLoaderdue to the large number of controllers/services.
For tegg, the bottleneck shifts to Node’s own GC and async hooks.
Profile – Memory
When the number of controllers/services is high, each controller instantiation consumes significant memory, creating a memory allocation bottleneck.
Current memory pressure resides mainly in
async_hook.
How to Leap
tegg Injection Principle
Example:
HelloWorldControllerinjects
Foo, which injects
Tracer.
<code>@HTTPController()
class HelloWorldController {
@Inject()
private readonly foo: Foo;
}
@Context()
class Foo {
@Inject()
private readonly tracer: Tracer;
}
</code>When a request enters the framework, the entry class (e.g.,
HelloWorldController) is located and all dependent objects are instantiated.
tegg v1 Performance Bottleneck
Each request creates 10,001 objects, causing heavy memory allocation and high GC pressure.
tegg v3 Optimization Principle
Reduce object instantiation: only
Tracerneeds per‑request context;
HelloWorldControllerand
Foocan be singletons, improving CPU and memory usage.
AsyncLocalStorage proxies objects so that singleton controllers retrieve the correct per‑request
Tracervia a context storage.
Optimization Results
Object count drops from 10,001 to 0, dramatically lowering memory pressure; tegg v1 heap fluctuated between 200 MB and 1 GB, while tegg v3 stabilizes around 200 MB.
tegg v3 Code Refactor
Change Annotation
Replace
@ContextProto()with
@SingletonProto:
<code>~~@ContextProto()~~
@SingletonProto()
class Foo {
@Inject()
private readonly tracer: Tracer;
}
</code>Maintain Stateful Context
If a class holds state tied to the request context, it must remain
@ContextPrototo avoid sharing state across requests.
<code>@ContextProto()
class Foo {
state: State;
foo() {
this.state = 'foo';
}
bar() {
this.state = 'bar';
}
}
</code>Unit Test Refactor
<code>describe('test/index.test.ts', () => {
let foo: Foo;
beforeEach(async () => {
foo = await app.getEggObject(Foo);
});
it('should work', () => {
assert(foo.hello());
});
});
</code>Alipay Experience Technology
Exploring ultimate user experience and best engineering practices
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.