Achtergrond
Een tijdje geleden kon de Cal.com app last hebben van lange laadtijden bij koude starts, variërend van 7 seconden tot 30 seconden in extreme gevallen. Dit gedrag komt niet altijd voor door de aard van koude starts, maar het deed zich vaak genoeg voor om een probleem te worden dat we zo snel mogelijk moesten aanpakken.
Voor degenen die niet bekend zijn met koude starts, zijn er veel materialen beschikbaar die het probleem in het algemeen beschrijven, dus daarop zullen we niet ingaan. Wat we hopen te bereiken met deze blog is de exacte oorzaak uit te leggen die ons systeem specifiek beïnvloedde, wat werd veroorzaakt door koude starts in een serverless omgeving.
Bevindingen en Experimenten
Laten we beginnen met de volgende log van Vercel:

Voor de /api/trpc/[trpc]
functie zien we een Koude Boot Duur van 298ms, maar een Uitvoeringsduur van 7.57s. Dit is een beetje vreemd, toch? De koude boot zelf is niet het probleem, maar in plaats daarvan is het wat er tijdens de koude boot wordt geladen dat de Uitvoeringsduur ernstig verhoogt.
Waar beginnen we om de oorzaak te vinden?
Hypothese 1: Grootte van functie-pakket
Na het zien van de Grootte van Functie van 33.1 MB, dook ons team, de gemeenschap en de mensen van Vercel en Prisma rechtstreeks in de bundle-analyse en compilatielogboeken om te proberen te achterhalen welke pakketten onze functies verzwaarde. Hoewel deze Grootte van Functie onder de limiet van 50MB ligt, geloofden we dat het nog steeds ten minste gedeeltelijk de schuldige kon zijn.
We identificeerden een paar grote afhankelijkheden die als onderdeel van onze sessie-ophaling werden geladen en die konden worden verminderd (PR) en we lazy loaded onze afhankelijkheden voor de app store (PR).
De prestaties verbeterden bij elke aanvraag na deze updates, maar het was niet de meervoudige vermindering van seconden waar we op hoopten. Wat zou het nog meer kunnen zijn? Is Prisma traag in verbinding maken bij koude starts in onze serverless setup?
Hypothese 2: Prisma
We hadden al Prisma Data Proxy in productie draaien om ons verbindingspoolbeheer af te handelen, dus we waren sceptisch dat het het probleem was, maar desondanks wilden we onze zorgvuldigheid betrachten om onszelf daarvan te verzekeren.
In een experimentele branch hebben we een test-eindpunt opgezet dat gescheiden was van onze API-routes en pagina's in een aparte app route (gebruik makend van de nieuwe App Router van Next.js) die alleen het aantal gebruikers in het systeem telde. De prestaties van deze route bij koude starts konden oplopen tot 1 seconde, wat ons liet zien dat Prisma een lichte vertraging kan hebben bij koude starts, maar opnieuw, het was niet de 7-15 seconden die we gemiddeld zagen.
Hypothese 3: _app en _document
Uit de uitgevoerde bundle-analyse bleek dat onze app een hoop afhankelijkheden laadt. Dat triggerde het idee om te verminderen wat er bij elke API-aanroep werd geladen. Wetende dat API-routes in Next.js het _app.tsx bestand laden, besloten we een PageWrapper te implementeren die alle afhankelijkheden bevat die nodig zijn voor niet-API-routes (PR).
Wederom is dit een mooie verbetering op het gebied van afhankelijkheidsoptimalisatie, maar we hadden nog steeds lange wachttijden.
Oorzaak
Na het verzenden van hoeveelheden prestatie-specifieke PRs die hielpen bij code-organisatie, snelheid en pakketgrootte, hadden we nog steeds de gouden oplossing niet gevonden. Het was tijd om weer te rond te draaien en te denken in termen van knelpunten, omdat veel prestatieproblemen gerelateerd zijn aan knelpunten, toch?
Met deze denkwijze werd er een test uitgevoerd om de eenvoudigste tRPC-router op te splitsen in zijn eigen API-route. Voor ons was dat onze openbare router. Na de implementatie werd het /api/trpc/viewer.public.i18n
eindpunt nu /api/trpc/public/i18n
. We zagen een afname van 15 seconden naar 2 seconden in koude starttijden voor deze route. Dit leek erop te wijzen dat onze enkele tRPC-router inderdaad de bottleneck was.
Eenvoudig gezegd, we hadden te veel tRPC-routers die in de hoofdviewer-router gingen die werd blootgesteld op de /api/trpc/[trpc]
route. Dit betekende dat de allereerste oproep naar deze functie, of het nu was om i18n-termen te laden of om een gebruikersschema op te vragen, alle 20 routers en al hun afhankelijkheden moest importeren.
Gezien hier in wat uiteindelijk de PR was die de meeste van onze lange koude startproblemen oploste, was er een vermoeden dat de slots-router te groot was en de oorzaak van de problemen was. Helaas, na het splitsen van de slots-router in zijn eigen Next.js API-route, zagen we nog steeds grote pakketgroottes en lange koude starttijden. Om deze redenen bleef deze PR onaangeroerd terwijl andere hypothesen werden nagestreefd.
Na de ontdekking dat de i18n-route in 2 seconden kon worden geladen bij koude starts, werd de eerder genoemde PR verder ontwikkeld zodat alle tRPC-routers hun eigen API-route hadden. Wanneer ze werden gesplitst, creëert Vercel aparte functies per route.
Waarom lost het splitsen van de tRPC-routers in hun eigen Next.js API-routes het probleem op? Het simpele antwoord, voor onze zaak, is dat zelfs als een pagina 5 verschillende tRPC-routes aanroept om gegevens te laden, ze gelijktijdig zullen draaien en dus het effect van het laden van afhankelijkheden voor alle routers niet wordt opgeteld. We kunnen nu nog steeds koude starttijden van 2-3 seconden zien op elke gegeven tRPC-route, maar aangezien ze gelijktijdig door de browser worden aangeroepen, kan de totale wachttijd 3 seconden zijn, niet 7-15 seconden (of 30 seconden in de extreme gevallen).
We erkennen dat er nog meer werk te verrichten is om deze snelheden nog verder te verminderen, maar de verminderingen tot nu toe zijn aanzienlijk geweest.
Hier is een probleem om potentiële veranderingen voor tRPC in serverless omgevingen te bevorderen.
Belangrijkste Resultaten

Verzendingen
Een grote dank aan iedereen die heeft bijgedragen aan het oplossen van dit probleem. Van het schrijven van aangepaste scripts die hielpen bij het bepalen welke afhankelijkheden ons systeem belastten tot het openen van PRs, we waarderen de ondersteuning en inspanning die zijn geleverd om deze problemen op te lossen. We hopen dat deze blog en de PRs een beetje inzicht kunnen bieden in de experimenten die we hebben uitgevoerd en de oorzaak die we uiteindelijk hebben gevonden.
Speciale dank aan Julius Marminge van tRPC.io, Guillermo Rauch, Malte Ubl en Jimmy Lai van Vercel en Lucas Smith. 🙏
Wat is de volgende stap
Hoewel dit een enorme prestatie is, werken we nog steeds hard om de koude starts nog verder te verminderen, maar ook om de zelfhosting ervaring zonder een serverless architectuur te verbeteren. Blijf op de hoogte!