幾個月前,我們發布了 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中文網其他相關文章!