ホームページ > 記事 > ウェブフロントエンド > Encore.ts — ElysiaJS や Hono よりも高速
几个月前,我们发布了 Encore.ts — TypeScript 的开源后端框架。
由于已经有很多框架,我们想分享一些我们做出的不常见的设计决策以及它们如何带来显着的性能数据。
我们之前发布的基准测试显示 Encore.ts 比 Express 快 9 倍,比 Fastify 快 2 倍。
这次我们将 Encore.ts 与 ElysiaJS 和 Hono 这两个现代高性能 TypeScript 框架进行了基准测试。
我们对带有和不带有模式验证的每个框架进行了基准测试,使用 TypeBox 与 ElsyiaJS 和 Hono 进行验证,因为它是这些框架原生支持的验证库。 (Encore.ts 有自己的内置类型验证,可以端到端工作。)
对于每个基准测试,我们都采取了五次运行的最佳结果。每次运行都是通过 150 个并发工作线程发出尽可能多的请求来执行,时间超过 10 秒。负载生成是使用 oha 执行的,oha 是一个基于 Rust 和 Tokio 的 HTTP 负载测试工具。
废话不多说,让我们看看数字!
(查看 GitHub 上的基准代码。)
除了性能之外,Encore.ts 在实现这一点的同时还保持了与 Node.js100% 的兼容性
。这怎么可能?通过我们的测试,我们确定了性能的三个主要来源,所有这些都与 Encore.ts 的底层工作方式有关。
Node.js 使用单线程事件循环运行 JavaScript 代码。尽管其具有单线程性质,但在实践中它具有相当大的可扩展性,因为它使用非阻塞 I/O 操作,并且底层 V8 JavaScript 引擎(也为 Chrome 提供动力)经过了极其优化。
但是你知道什么比单线程事件循环更快吗?多线程。
Encore.ts 由两部分组成:
Encore Runtime 处理所有 I/O,例如接受和处理传入的 HTTP 请求。它作为一个完全独立的事件循环运行,利用底层硬件支持的尽可能多的线程。
请求被完全处理和解码后,它会被移交给 Node.js 事件循环,然后从 API 处理程序获取响应并将其写回客户端。
(在你说之前:是的,我们在你的事件循环中放置了一个事件循环,这样你就可以在事件循环的同时进行事件循环。)
Encore.ts,顾名思义,是专为 TypeScript 设计的。但您实际上无法运行 TypeScript:它首先必须通过剥离所有类型信息来编译为 JavaScript。这意味着运行时类型安全更难实现,这使得验证传入请求之类的事情变得困难,导致像 Zod 这样的解决方案在运行时定义 API 模式变得流行。
Encore.ts 的工作方式有所不同。通过 Encore,您可以使用本机 TypeScript 类型定义类型安全的 API:
import { api } from "encore.dev/api"; interface BlogPost { id: number; title: string; body: string; likes: number; } export const getBlogPost = api( { method: "GET", path: "/blog/:id", expose: true }, async ({ id }: { id: number }) => Promise<BlogPost> { // ... }, );
Encore.ts 然后解析源代码以了解每个 API 端点期望的请求和响应模式,包括 HTTP 标头、查询参数等。然后,对架构进行处理、优化并存储为 Protobuf 文件。
当 Encore Runtime 启动时,它会读取此 Protobuf 文件并预先计算请求解码器和响应编码器,并使用每个 API 端点期望的确切类型定义,针对每个 API 端点进行优化。事实上,Encore.ts 甚至直接在 Rust 中处理请求验证,确保无效请求永远不必接触 JS 层,从而减轻许多拒绝服务攻击。
Encore 对请求模式的理解从性能角度来看也证明是有益的。像 Deno 和 Bun 这样的 JavaScript 运行时使用与 Encore 基于 Rust 的运行时类似的架构(事实上,Deno 也使用 Rust Tokio Hyper),但缺乏 Encore 对请求模式的理解。因此,他们需要将未处理的 HTTP 请求交给单线程 JavaScript 引擎执行。
Encore.ts, on the other hand, handles much more of the request processing inside Rust, and only hands over the decoded request objects. By handling much more of the request life-cycle in multi-threaded Rust, the JavaScript event-loop is freed up to focus on executing application business logic instead of parsing HTTP requests, yielding an even greater performance boost.
Careful readers might have noticed a trend: the key to performance is to off-load as much work from the single-threaded JavaScript event-loop as possible.
We've already looked at how Encore.ts off-loads most of the request/response lifecycle to Rust. So what more is there to do?
Well, backend applications are like sandwiches. You have the crusty top-layer, where you handle incoming requests. In the center you have your delicious toppings (that is, your business logic, of course). At the bottom you have your crusty data access layer, where you query databases, call other API endpoints, and so on.
We can't do much about the business logic — we want to write that in TypeScript, after all! — but there's not much point in having all the data access operations hogging our JS event-loop. If we moved those to Rust we'd further free up the event loop to be able to focus on executing our application code.
So that's what we did.
With Encore.ts, you can declare infrastructure resources directly in your source code.
For example, to define a Pub/Sub topic:
import { Topic } from "encore.dev/pubsub"; interface UserSignupEvent { userID: string; email: string; } export const UserSignups = new Topic<UserSignupEvent>("user-signups", { deliveryGuarantee: "at-least-once", }); // To publish: await UserSignups.publish({ userID: "123", email: "hello@example.com" });
"So which Pub/Sub technology does it use?"
— All of them!
The Encore Rust runtime includes implementations for most common Pub/Sub technologies, including AWS SQS+SNS, GCP Pub/Sub, and NSQ, with more planned (Kafka, NATS, Azure Service Bus, etc.). You can specify the implementation on a per-resource basis in the runtime configuration when the application boots up, or let Encore's Cloud DevOps automation handle it for you.
Beyond Pub/Sub, Encore.ts includes infrastructure integrations for PostgreSQL databases, Secrets, Cron Jobs, and more.
All of these infrastructure integrations are implemented in the Encore.ts Rust Runtime.
This means that as soon as you call .publish(), the payload is handed over to Rust which takes care to publish the message, retrying if necessary, and so on. Same thing goes with database queries, subscribing to Pub/Sub messages, and more.
The end result is that with Encore.ts, virtually all non-business-logic is off-loaded from the JS event loop.
In essence, with Encore.ts you get a truly multi-threaded backend "for free", while still being able to write all your business logic in TypeScript.
Whether or not this performance is important depends on your use case. If you're building a tiny hobby project, it's largely academic. But if you're shipping a production backend to the cloud, it can have a pretty large impact.
Lower latency has a direct impact on user experience. To state the obvious: A faster backend means a snappier frontend, which means happier users.
Higher throughput means you can serve the same number of users with fewer servers, which directly corresponds to lower cloud bills. Or, conversely, you can serve more users with the same number of servers, ensuring you can scale further without encountering performance bottlenecks.
While we're biased, we think Encore offers a pretty excellent, best-of-all-worlds solution for building high-performance backends in TypeScript. It's fast, it's type-safe, and it's compatible with the entire Node.js ecosystem.
And it's all Open Source, so you can check out the code and contribute on GitHub.
Or just give it a try and let us know what you think!
以上がEncore.ts — ElysiaJS や Hono よりも高速の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。