Vor ein paar Monaten haben wir Encore.ts veröffentlicht – ein Open-Source-Backend-Framework für TypeScript.
Da es bereits viele Frameworks gibt, wollten wir einige der ungewöhnlichen Designentscheidungen, die wir getroffen haben, und wie sie zu bemerkenswerten Leistungszahlen führen, mit Ihnen teilen.
Wir haben bereits Benchmarks veröffentlicht, die zeigen, dass Encore.ts 9x schneller als Express und 2x schneller als Fastify ist.
Dieses Mal haben wir Encore.ts mit ElysiaJS und Hono verglichen, zwei modernen Hochleistungs-TypeScript-Frameworks.
Wir haben jedes Framework sowohl mit als auch ohne Schemavalidierung einem Benchmarking unterzogen und dabei TypeBox für die Validierung mit ElsyiaJS und Hono verwendet, da es sich um eine nativ unterstützte Validierungsbibliothek für diese Frameworks handelt. (Encore.ts verfügt über eine eigene integrierte Typvalidierung, die durchgängig funktioniert.)
Für jeden Benchmark haben wir das beste Ergebnis aus fünf Läufen erzielt. Bei jedem Lauf wurden so viele Anfragen wie möglich mit 150 gleichzeitigen Arbeitern über einen Zeitraum von mehr als 10 Sekunden gestellt. Die Lastgenerierung wurde mit oha durchgeführt, einem auf Rust und Tokio basierenden HTTP-Lasttest-Tool.
Genug geredet, schauen wir uns die Zahlen an!
(Schauen Sie sich den Benchmark-Code auf GitHub an.)
Neben der Leistung erreicht Encore.ts dies unter Beibehaltung von 100 % Kompatibilität mit Node.js.
Wie ist das möglich? Bei unseren Tests haben wir drei Hauptleistungsquellen identifiziert, die alle damit zusammenhängen, wie Encore.ts unter der Haube funktioniert.
Node.js führt JavaScript-Code mithilfe einer Single-Threaded-Ereignisschleife aus. Trotz seiner Single-Thread-Natur ist dies in der Praxis recht skalierbar, da es nicht blockierende I/O-Vorgänge verwendet und die zugrunde liegende V8-JavaScript-Engine (die auch Chrome antreibt) extrem optimiert ist.
Aber wissen Sie, was schneller ist als eine Single-Threaded-Ereignisschleife? Ein Multithread.
Encore.ts besteht aus zwei Teilen:
Ein TypeScript-SDK, das Sie beim Schreiben von Backends mit Encore.ts verwenden.
Eine Hochleistungslaufzeit mit einer asynchronen Multithread-Ereignisschleife, geschrieben in Rust (unter Verwendung von Tokio und Hyper).
Die Encore-Runtime verarbeitet alle E/A-Vorgänge wie das Akzeptieren und Verarbeiten eingehender HTTP-Anfragen. Dies läuft als völlig unabhängige Ereignisschleife, die so viele Threads nutzt, wie die zugrunde liegende Hardware unterstützt.
Sobald die Anfrage vollständig verarbeitet und dekodiert wurde, wird sie an die Node.js-Ereignisschleife übergeben und nimmt dann die Antwort vom API-Handler entgegen und schreibt sie zurück an den Client.
(Bevor Sie es sagen: Ja, wir fügen eine Ereignisschleife in Ihre Ereignisschleife ein, sodass Sie eine Ereignisschleife ausführen können, während Sie eine Ereignisschleife ausführen.)
Encore.ts wurde, wie der Name schon sagt, von Grund auf für TypeScript entwickelt. Sie können TypeScript jedoch nicht wirklich ausführen: Es muss zunächst in JavaScript kompiliert werden, indem alle Typinformationen entfernt werden. Dies bedeutet, dass die Typsicherheit zur Laufzeit viel schwieriger zu erreichen ist, was es schwierig macht, beispielsweise eingehende Anfragen zu validieren, was dazu führt, dass Lösungen wie Zod stattdessen für die Definition von API-Schemas zur Laufzeit populär werden.
Encore.ts funktioniert anders. Mit Encore definieren Sie typsichere APIs mithilfe nativer TypeScript-Typen:
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 analysiert dann den Quellcode, um das Anforderungs- und Antwortschema zu verstehen, das jeder API-Endpunkt erwartet, einschließlich Dinge wie HTTP-Header, Abfrageparameter usw. Die Schemata werden dann verarbeitet, optimiert und als Protobuf-Datei gespeichert.
Wenn die Encore Runtime startet, liest sie diese Protobuf-Datei und berechnet vorab einen Anforderungsdecoder und einen Antwortencoder, die für jeden API-Endpunkt optimiert sind, und zwar unter Verwendung der genauen Typdefinition, die jeder API-Endpunkt erwartet. Tatsächlich übernimmt Encore.ts sogar die Anforderungsvalidierung direkt in Rust und stellt so sicher, dass ungültige Anforderungen niemals die JS-Ebene berühren müssen, wodurch viele Denial-of-Service-Angriffe abgemildert werden.
Encores Verständnis des Anforderungsschemas erweist sich auch aus Leistungssicht als vorteilhaft. JavaScript-Laufzeiten wie Deno und Bun verwenden eine ähnliche Architektur wie die auf Rust basierende Laufzeit von Encore (tatsächlich verwendet Deno auch Rust Tokio Hyper), ihnen fehlt jedoch das Verständnis von Encore für das Anforderungsschema. Daher müssen sie die unverarbeiteten HTTP-Anfragen zur Ausführung an die Single-Threaded-JavaScript-Engine übergeben.
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!
The above is the detailed content of Encore.ts — faster than ElysiaJS & Hono. For more information, please follow other related articles on the PHP Chinese website!