展示 HN:Elysia JIT “编译器”,为什么它是最快的 JavaScript 框架之一
Show HN: Elysia JIT "Compiler", why it's one of the fastest JavaScript framework

原始链接: https://elysiajs.com/internal/jit-compiler

## Elysia: 一款高性能 JavaScript 框架 Elysia 是一款非常快速的 JavaScript Web 框架,始终名列前茅——其速度主要受 JavaScript 引擎本身的限制。它通过在 0.4 版本中引入独特的即时编译 (JIT) “编译器” 实现这一点,该编译器会根据定义的路由和中间件动态生成优化的请求处理代码。 与传统编译器不同,Elysia 的 JIT 使用 `new Function()` 为每个路由即时创建定制代码,从而最大限度地减少不必要的处理。这得益于“Sucrose”,一个静态代码分析模块,它可以精确识别路由处理程序需要哪些请求数据(参数、主体等),从而使编译器可以跳过解析未使用的元素。 Elysia 还通过平台特定功能(如 Bun 的原生路由)和紧凑的响应映射等技术进一步优化性能。虽然 JIT 会引入少量初始开销(每个路由小于 0.005 毫秒),但可以通过预编译来缓解。 尽管捆绑包尺寸略大且代码库复杂度增加,但 Elysia 的性能提升——通过 TechEmpower 等基准测试验证——是巨大的,使其成为构建高性能服务器的理想选择。

## Elysia:一款快速的 JavaScript 框架,带有 JIT “编译器” 一款名为 Elysia 的新型 JavaScript 框架正因其卓越的性能而备受关注,据称在 Bun 运行时上速度最快,在 Node、Deno 和 Cloudflare Workers 上也具有竞争力。这种速度源于一个嵌入式的 JIT “编译器”——一种先前用于其他项目输入验证的技术——Elysia 将其扩展到完整的后端框架。 JIT 编译器分析代码,以确定请求的哪些部分是必需的,从而跳过不必要的数据解析,例如请求体或头部。虽然提供了显著的优势,但一些评论员对潜在的安全漏洞表示担忧,尤其是在输入由用户控制的情况下。 讨论还集中在性能提升是否真的能被最终用户感知到,以及由此带来的复杂性是否值得。 还有人质疑诸如常量折叠之类的优化的价值,认为现代 JavaScript 引擎可能已经执行了这些任务。 该框架对子函数和复杂逻辑的处理也值得进一步调查。
相关文章

原文

Are you an LLM? You can read better optimized documentation at /internal/jit-compiler.md for this page in Markdown format

Elysia is fast and will likely remain one of the fastest web frameworks for JavaScript only limited by the speed of the underlying JavaScript engine.

  1. Elysia Bun

    2,454,631 reqs/s

  2. Gin Go

    676,019

  3. Spring Java

    506,087

  4. Fastify Node

    415,600

  5. Express Node

    113,117

  6. Nest Node

    105,064

Measured in requests/second. Result from TechEmpower Benchmark Round 22 (2023-10-17) in PlainText

Elysia speed is not only acheived by optimization for specific runtime eg. Bun native features like Bun.serve.routes. But also the way Elysia handles route registration and request handling.

Elysia has an JIT "compiler" embedded within its core since Elysia 0.4 (30 Mar 2023) at (src/compose.ts) using new Function(...) or also known as eval(...).

The "compiler" is not a traditional compiler that translates code from one language to another. Instead, it dynamically generates optimized code for handling requests based on the defined routes and middleware. (Which is why we put compiler in quotes.)

When request is made to Elysia application for the first time for each route, Elysia dynamically generates optimized code specifically tailored to handle that route efficiently on the fly avoiding unnecessary overhead as much as possible.

Static Code Analysis (Sucrose)

"Sucrose" is the nick name for the static code analysis module living alongside Elysia's JIT "compiler" at (src/sucrose.ts).

To generate this optimized code, the compiler needs a deep understanding of how the route handlers interact with the request and what parts of the request are actually needed.

That's Sucrose's job.

Sucrose read the code without executing it by using Function.toString() then perform our own custom pattern-matching to extract useful information about what parts of the request are actually needed by the route handler.

Let's take a look at a simple example:

In this code, we can clearly see that this handler only need a params to be parsed.

Sucrose looks at code and tells the "compiler" to only parse params and skip parsing other parts of the request like body, query, headers entirely as it's not need.

JIT "compiler" then generates code like this:

This approach is entirely different from traditional web frameworks that parse everything by default with a centralHandler regardless of whether it's needed or not which looks something like this:

This make Elysia extremely fast as it only does the minimum work required for each route.

Why not acorn, esprima, or other traditional static analysis tools?

Traditional tools are designed for general-purpose static code analysis and may introduce unnecessary overhead for Elysia's specific use case.

For our purpose, our parser only need to understand a subset of JavaScript syntax specifically function. When we think about it, it's only a small part of JavaScript language that is already parsed and formatted by JavaScript Engine.

So instead of pulling a general purpose tool, we treat this part as a DSL (that looks like JavaScript) and build specifically for just this part for maximum performance and low-memory usage (compared to AST-based tools).

Compiler Optimizations

Similar to traditional compilers, Elysia's JIT "compiler" also performs various optimizations to further enhance the performance of the generated code like optimizing control flow based on the specific usage patterns of the route handlers, constant fold, using direct access to properties instead of iterating through objects and arrays when possible, and more.

These optimizations and much smaller optimizations help to reduce the overhead of request handling and improve the overall speed of the application.

Example: mapResponse, mapCompactResponse

This is one of the smaller optimizations but can have a significant impact on performance in high-throughput scenarios.

Elysia has two special optimizations for response mapping functions: mapResponse and mapCompactResponse.

Constructing a new Response object can be relatively expensive but for new Response without any additional status or headers is cheaper than constructing a full Response object with custom status codes or headers.

When set or status is not used, Elysia will use mapCompactResponse to map a value directly to a Response object without the overhead of additional properties.

Platform Specific Optimization

Elysia is originally made specifically for Bun but also works on Node.js, Deno, Cloudflare Workers and more.

There are a big difference between being compatible and being optimized for a specific platform.

Elysia can take advantage of platform-specific features and optimizations to further enhance performance, for example Bun.serve.routes is used when running on Bun to leverage Bun's native routing capabilities which is written in Zig for maximum performance.

Using the inline response for maximum performance for static responses which made Elysia the rank at #14 on TechEmpower Framework Benchmarks among the world's fastest backend frameworks.

There are more various smaller optimization like

  • using Bun.websocket when running on Bun for optimal WebSocket performance
  • Elysia.file conditionally use Bun.file when available for faster file handling
  • using Headers.toJSON() when running on Bun to reduce overhead when dealing headers

These small optimizations add up to make Elysia extremely fast on its target platforms.

Overhead of JIT "Compiler"

Elysia JIT "compiler" is designed for peak performance in mind. However, the dynamic code generation process does introduce some overhead during the initial request handling for each route.

Initial Request Overhead

The first time a request is made to a specific route, Elysia needs to analyze the route handler code and generate the optimized code.

This process is relatively very fast and usually takes < 0.005ms per route in most cases on a modern CPU and happend only once per route. But it is still an overhead.

??? ms/compilations

Running Elysia in your browser with 10,000 compilations took ??? ms

This process can be moved to the startup phase by settings precompile: true to Elysia constructor to eliminate this overhead during the first request in exchange for a slower startup time.

Memory Usage

The dynamically generated code is stored in memory for subsequent requests. This can lead to increased memory usage, especially for applications with a large number of routes but is relatively low.

Bigger Bundle Size

The JIT "compiler" and Sucrose module add some additional code to the Elysia core library, which can increase the overall bundle size of the application. However, the performance benefits often outweigh the cost of a slightly larger bundle size.

Maintainability

The use of dynamic code generation can make the codebase more complex and harder to maintain. Maintainers need to have a good understanding of how the JIT "compiler" works to effectively use and troubleshoot the framework.

Security Considerations

Using new Function(...) or eval(...) can introduce security risks if not handled properly.

But that's only "if not handled properly" part.

Elysia takes precautions to ensure that the generated code is safe and does not expose vulnerabilities by make sure that only trusted code is executed. The input is almost never user-controlled and produced by Elysia (sucrose) itself.

Libraries that eval

Elysia is not the only framework that use new Function and eval.

ajv and TypeBox are an industry standard validation library since the early days of Node.js with 895m and 332m downloads/months respectively.

Both of these libraries are using eval internally to optimize the performance of their validation code making it faster its competitors.

Elysia basically expands this beyond input validation into a whole backend framework for maximum performance. In fact, Elysia also use TypeBox for input validation, so every corner of the libraries is entirely runs on eval.

Opts out

Elysia JIT compilation is enabled by default but can be opt out entirely by running in a dynamic mode:

Although, it's not recommended because there are some features missing without JIT compilation, eg. trace.

Afterword

With all of these overkills optimization, Elysia manages to have almost zero overhead and the only limiting factor is the speed of the underlying JavaScript engine itself.

Despite the maintainability challenges, the trade-offs made by Elysia's JIT "compiler" are worth it for the significant performance gains it provides and aligns with our goal to provide a fast foundation for building high-performance server.

This can also be seen as a differentiating factor for Elysia compared to other web frameworks that may not prioritize performance to the same extent because it's extremely hard to do properly.

We also has a short 6-pages research paper we published to ACM Digital Library about Elysia's JIT "compiler" and its performance optimizations.

For over years of Elysia existence, we almost never saw a valid benchmark where Elysia is not the fastest framework available on a platform except using a FFI/native binding (eg. Rust, Go, Zig) with a valid benchmark.

Which is still relatively a very hard to beat because of serialization/deserialization overhead. There are some cases like uWebSocket which is written in C++ with JavaScript binding, making it extremely fast that outperform Elysia.

But despite all odds, we think it's worth it.

联系我们 contact @ memedata.com