Encore.ts — 比 Express.js 快 9 倍,比 Bun + Zod 快 3 倍

2025-05-27

Encore.ts — 比 Express.js 快 9 倍,比 Bun + Zod 快 3 倍

几个月前,我们发布了Encore.ts——一个 TypeScript 的开源后端框架。

由于已经有很多框架,我们想分享一些我们做出的异常设计决策以及它们如何带来显著的性能数字。

性能基准

我们对 Encore.ts、Bun、Fastify 和 Express 进行了基准测试,包括有和没有模式验证的情况。

对于模式验证,我们尽可能使用 Zod。对于 Fastify,我们使用 Ajv 作为官方支持的模式验证库。

对于每个基准测试,我们取了五次运行中的最佳结果。每次运行都使用 150 个并发工作器在 10 秒内发出尽可能多的请求。负载生成由oha(一款基于 Rust 和 Tokio 的 HTTP 负载测试工具)执行。

说得够多了,让我们看看数字吧!

Encore.ts 每秒处理的请求数比 Express.js 多 9 倍

每秒请求数

Encore.ts 的响应延迟比 Express.js 减少了 80%

响应延迟

查看GitHub上的基准代码。

除了性能之外,Encore.ts 还实现了这一点,同时保持了与 Node.js 100% 的兼容性

这怎么可能?通过测试,我们发现了三个主要的性能问题,都与 Encore.ts 的底层工作原理有关。

提升 #1:在你的事件循环中添加一个事件循环

Node.js 使用单线程事件循环运行 JavaScript 代码。尽管是单线程的,但在实践中却具有相当强的可扩展性,因为它使用非阻塞 I/O 操作,并且底层的 V8 JavaScript 引擎(也支持 Chrome)经过了高度优化。

但是你知道什么比单线程事件循环更快吗?多线程的。

Encore.ts由两部分组成:

  1. 使用 Encore.ts 编写后端时使用的 TypeScript SDK。

  2. 高性能运行时,具有用 Rust 编写的多线程、异步事件循环(使用TokioHyper)。

Encore Runtime 处理所有 I/O,例如接受和处理传入的 HTTP 请求。它以完全独立的事件循环形式运行,并利用底层硬件支持的线程数量。

一旦请求被完全处理和解码,它就会被移交给 Node.js 事件循环,然后从 API 处理程序获取响应并将其写回客户端。

(在您说之前:是的,我们在您的事件循环中放置了一个事件循环,因此您可以在事件循环时进行事件循环。)

图表

提升 #2:预先计算请求模式

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> {
        // ...
    },
);


Enter fullscreen mode Exit fullscreen mode

然后,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 在 Rust 内部处理了更多的请求处理,并且只将解码后的请求对象移交给 Rust。通过在多线程 Rust 中处理更多的请求生命周期,JavaScript 事件循环可以专注于执行应用程序业务逻辑,而不是解析 HTTP 请求,从而带来更大的性能提升。

推动力3:基础设施整合

细心的读者可能已经注意到一个趋势:性能的关键是尽可能地从单线程 JavaScript 事件循环中卸载尽可能多的工作。

我们已经了解了 Encore.ts 如何将大部分请求/响应生命周期的工作转移到 Rust。那么接下来还有什么可以做的呢?

后端应用程序就像三明治。最外层是一层脆皮,用于处理传入的请求。中间是美味的配料(当然,也就是业务逻辑)。最底层是一层脆皮数据访问层,用于查询数据库、调用其他 API 端点等等。

我们对业务逻辑无能为力——毕竟我们想用 TypeScript 来写!——但让所有数据访问操作都占用 JS 事件循环也没什么意义。如果我们把这些操作迁移到 Rust,就能进一步释放事件循环,让我们能够专注于执行应用程序代码。

所以我们就是这么做的。

使用 Encore.ts,您可以直接在源代码中声明基础设施资源。

例如,定义一个 Pub/Sub 主题:



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" });


Enter fullscreen mode Exit fullscreen mode

“那么它使用哪种 Pub/Sub 技术呢?”
——全部!

Encore Rust 运行时包含大多数常见 Pub/Sub 技术的实现,包括 AWS SQS+SNS、GCP Pub/Sub 和 NSQ,以及更多计划中的技术(例如 Kafka、NATS、Azure Service Bus 等)。您可以在应用程序启动时在运行时配置中为每个资源指定实现,也可以让 Encore 的 Cloud DevOps 自动化功能为您处理。

除了 Pub/Sub 之外,Encore.ts 还包括 PostgreSQL 数据库、Secrets、Cron Jobs 等基础设施集成。

所有这些基础设施集成都是在 Encore.ts Rust Runtime 中实现的。

这意味着,只要你调用.publish(),有效载荷就会被交给 Rust,Rust 会负责发布消息、必要时重试等等。数据库查询、订阅 Pub/Sub 消息等操作也是如此。

最终结果是,使用 Encore.ts,几乎所有非业务逻辑都从 JS 事件循环中卸载。

图表

本质上,使用 Encore.ts,您可以“免费”获得真正的多线程后端,同时仍然能够用 TypeScript 编写所有业务逻辑。

结论

这种性能是否重要取决于你的用例。如果你正在构建一个小型的业余项目,它主要还是出于学术目的。但如果你将生产后端迁移到云端,它可能会产生相当大的影响。

更低的延迟直接影响用户体验。显而易见,更快的后端意味着更流畅的前端,从而带来更愉悦的用户体验。

更高的吞吐量意味着您可以用更少的服务器服务相同数量的用户,这直接意味着更低的云费用。或者,相反,您可以用相同数量的服务器服务更多用户,从而确保您能够进一步扩展而不会遇到性能瓶颈。

虽然我们可能存在一些偏见,但我们认为 Encore 提供了一个非常出色、世界一流的解决方案,用于用 TypeScript 构建高性能后端。它速度快、类型安全,并且与整个 Node.js 生态系统兼容。

而且它全部都是开源的,因此您可以查看代码并在GitHub上做出贡献。

或者尝试一下并告诉我们您的想法!

文章来源:https://dev.to/encore/encorets-9x-faster-than-expressjs-3x-faster-than-bun-zod-4boe
PREV
Web 服务器是如何工作的?HTTP 我们做了什么?服务器是如何工作的?服务端点
NEXT
如何使用 Ollama、LangChain 在 Python 中本地运行 Llama-3.1🦙