Hintergrund
Seit einiger Zeit konnte die Cal.com-App unter langen Kaltstart-Ladezeiten leiden, die in extremen Fällen von 7 Sekunden bis zu 30 Sekunden reichten. Dieses Verhalten trat nicht immer aufgrund der Natur von Kaltstarts auf, aber es geschah häufig genug, dass es ein Problem wurde, das wir so schnell wie möglich angehen mussten.
Für diejenigen, die mit Kaltstarts nicht vertraut sind, gibt es viele Materialien, die das Problem im Allgemeinen beschreiben, daher werden wir das nicht näher behandeln. Was wir mit diesem Blog erreichen möchten, ist, die genaue Ursache zu erklären, die unser System im Besonderen betraf, die durch Kaltstarts in einer serverlosen Umgebung verursacht wurde.
Feststellungen und Experimente
Zunächst schauen wir uns das folgende Protokoll von Vercel an:

Für die /api/trpc/[trpc]
-Funktion sehen wir eine Kaltstart-Dauer von 298ms, aber eine Ausführungsdauer von 7,57s. Das ist ein bisschen seltsam, oder? Der Kaltstart selbst ist nicht das Problem, sondern das, was beim Kaltstart geladen wird, verursacht die starke Inflation der Ausführungsdauer.
Wo fangen wir also an, die Ursache zu finden?
Hypothese 1: Funktion-Bündelgröße
Nachdem wir die Funktion-Größe von 33,1 MB gesehen hatten, tauchten unser Team, die Community und die Leute von Vercel und Prisma direkt ein und begannen mit Bündelanalyse und Kompilierungsprotokollierung, um herauszufinden, welche Pakete unsere Funktionen belasteten. Auch wenn diese Funktion-Größe unter dem Limit von 50MB lag, glaubten wir, dass sie zumindest teilweise der Übeltäter sein könnte.
Wir identifizierten einige große Abhängigkeiten, die als Teil unserer Sitzungsabholung geladen wurden, die reduziert werden konnten (PR), und wir luden unsere Abhängigkeiten für den App-Store lazy (PR).
Die Leistung verbesserte sich bei jeder Anfrage nach diesen Updates, aber es war nicht die Mehrsekunden-Reduktion, die wir erhofft hatten. Was könnte es also noch sein? Ist Prisma langsam in der Verbindung ohne Kaltstart in unserem serverlosen Setup?
Hypothese 2: Prisma
Wir hatten bereits den Prisma Data Proxy in der Produktion aktiv, um unser Connection Pooling zu verwalten, daher waren wir skeptisch, dass es das Problem war. Dennoch wollten wir unser Pflichtbewusstsein tun, um uns dessen gewiss zu sein.
In einem experimentellen Zweig richteten wir einen Test-Endpunkt ein, der von unseren API-Routen und Seiten in einer separaten App-Route getrennt war (unter Verwendung des neuen App-Routers von Next.js), der nur die Anzahl der Benutzer im System abrief. Die Leistung dieser Route bei Kaltstarts konnte bis zu 1 Sekunde betragen, was uns zeigte, dass Prisma bei Kaltstarts eine leichte Verzögerung haben kann, aber es waren immer noch nicht die 7-15 Sekunden, die wir im Durchschnitt sahen.
Hypothese 3: _app und _document
Aus der durchgeführten Bündelanalyse war klar, dass unsere App eine Menge Abhängigkeiten lädt. Das brachte die Idee hervor, das, was bei jeder API-Anfrage geladen wurde, zu reduzieren. Da die API-Routen in Next.js die Datei _app.tsx laden, entschieden wir uns, einen PageWrapper zu implementieren, der alle für Nicht-API-Routen benötigten Abhängigkeiten enthält (PR).
Nochmals, das ist eine schöne Verbesserung in Bezug auf die Abhängigkeitsoptimierung, aber wir hatten immer noch unsere langen Wartezeiten.
Ursache
Nachdem wir eine Menge von leistungsbezogenen PRs veröffentlicht hatten, die bei der Code-Organisation, Geschwindigkeit und Bündelgröße halfen, hatten wir immer noch nicht das Patentrezept gefunden. Es war an der Zeit, den vollen Kreis zu schließen und in Bezug auf Engpässe zu denken, da viele Leistungsprobleme mit Engpässen verbunden sind, oder?
Mit diesem Gedanken wurde ein Test durchgeführt, um den einfachsten tRPC-Router in seine eigene API-Route zu teilen. Für uns war das unser öffentlicher Router. Nach dem Deployment war der Endpunkt /api/trpc/viewer.public.i18n
jetzt /api/trpc/public/i18n
. Wir sahen einen Rückgang von 15 Sekunden auf 2 Sekunden bei Kaltstartzeiten für diese Route. Das schien zu verdeutlichen, dass unser einzelner tRPC-Router tatsächlich der Engpass war.
Einfache gesagt, hatten wir zu viele tRPC-Router, die in den Hauptviewer-Router eingingen, der auf der Route /api/trpc/[trpc]
exponiert war. Das bedeutete, dass der allererste Aufruf dieser Funktion, sei es um i18n-Begriffe zu laden oder um den Zeitplan eines Benutzers abzurufen, alle 20 Router und deren Abhängigkeiten importieren musste.
Siehe hier in dem, was letztendlich der PR war, der die meisten unserer langen Kaltstartprobleme löste, gab es den Verdacht, dass der Slots-Router zu groß war und die Ursache der Probleme war. Leider sahen wir, nachdem wir den Slots-Router in seine eigene Next.js API-Route aufgeteilt hatten, immer noch große Bündelgrößen und lange Kaltstartzeiten. Aus diesen Gründen blieb dieser PR unbeachtet, während andere Hypothesen weiterverfolgt wurden.
Nach der Entdeckung, dass die i18n-Route bei Kaltstarts in 2 Sekunden geladen wurde, wurde der oben erwähnte PR dann weiter aufgebaut, sodass alle tRPC-Router ihre eigene API-Route hatten. Bei der Aufteilung erstellt Vercel separate Funktionen pro Route.
Warum behebt die Aufteilung der tRPC-Router in ihre eigenen Next.js API-Routen das Problem? Die einfache Antwort für unseren Fall ist, dass selbst wenn eine Seite 5 verschiedene tRPC-Routen aufruft, um Daten zu laden, sie parallel ausgeführt werden und somit die Auswirkungen des Ladens von Abhängigkeiten aller Router nicht kumuliert werden. Wir können jetzt immer noch Kaltstartzeiten von 2-3 Sekunden für jede tRPC-Route sehen, aber da sie von dem Browser gleichzeitig aufgerufen werden, kann die gesamte Wartezeit 3 Sekunden betragen, nicht 7-15 Sekunden (oder 30 Sekunden in extremen Fällen).
Wir erkennen an, dass es noch mehr Arbeit zu leisten gibt, um diese Geschwindigkeiten weiter zu reduzieren, aber die bisher erzielten Reduzierungen waren erheblich.
Hier ist ein Problem, um potenzielle Änderungen für tRPC in serverlosen Umgebungen zu fördern.
Wichtigste Ergebnisse

Danksagungen
Ein großer Dank und eine Danksagung an alle, die zur Lösung dieses Problems beigetragen haben. Von der Erstellung benutzerdefinierter Skripte, die halfen zu bestimmen, welche Abhängigkeiten unser System belasteten, bis hin zum Öffnen von PRs, schätzen wir die Unterstützung und den Einsatz, die zur Lösung dieser Probleme investiert wurden, wirklich. Wir hoffen, dass dieser Blog und die PRs ein wenig Einblick in die Experimente, die wir durchgeführt haben, und die Ursache, die wir schließlich gefunden haben, geben können.
Besonderer Dank geht an Julius Marminge von tRPC.io, Guillermo Rauch, Malte Ubl und Jimmy Lai von Vercel und Lucas Smith. 🙏
Was kommt als nächstes
Obwohl dies ein großer Erfolg war, arbeiten wir weiterhin hart daran, die Kaltstarts noch weiter zu reduzieren und auch die Selbsthosting-Erfahrung ohne serverlose Architektur zu verbessern. Bleiben Sie dran!