Langzame CI is een van die dingen die de motoren van engineeringteams vertraagt. Ingenieurs wachten langer op feedback, het wisselen tussen contexten neemt toe, en de verleiding om wijzigingen te bundelen groeit, wat de situatie alleen maar erger maakt. We hebben enkele weken besteed aan het optimaliseren van onze GitHub Actions-workflows en hebben onze PR-controles van 30 minuten tot 5 minuten teruggebracht. Dat is een vermindering van 83% in wachttijd, bereikt zonder concessies te doen aan testdekking of kwaliteitsborging.
Er was niet één magische oplossing. We keken naar waar de tijd daadwerkelijk naartoe ging en pasten drie principes toe: slimme taakafhankelijkheden, agressieve caching en testsharding. Dit is wat we geleerd hebben.
De Basis: Migreren naar Blacksmith
We hadden eigenlijk al geprobeerd te migreren naar Blacksmith, maar stuitten op problemen. Met een main branch zo actief als de onze, wilden we geen last hebben van CI-inconsistenties. Deze keer was de migratie rechttoe rechtaan (#26247): vervang buildjet-*vcpu-ubuntu-2204 runners door blacksmith-*vcpu-ubuntu-2404 in 28 workflowbestanden, vervang de cache en setup-node acties, en pas de vCPU-toewijzingen aan. Het enige echte probleem dat we tegenkwamen was het corrupte cacheprobleem met .next bestanden (meer hierover in de cachingsectie).
Toen we upgrade, merkten we onmiddellijk een 2x snelheidsverbetering bij het ophalen van de cache. Met yarn install die 1,2 GB is, bespaart dat ongeveer 22 seconden op elke taak die yarn install moet doen, wat de meeste is. We maken ook gebruik van Blacksmith's container caching voor services die in veel taken worden gebruikt (Postgres, Redis, Mailhog). De Initialize containers job is nu met meer dan 50% verminderd, van 20 seconden naar 9 seconden.
Wat we moesten oplossen
Blacksmith runners tonen het aantal CPU's van de hostmachine in plaats van de toegewezen vCPUs (typisch 4). Playwright, die behulpzaam is, zou workers aanmaken om bij te blijven, wat leidde tot resourcecontentie en onbetrouwbare testfalen. We moesten simpelweg het aantal workers expliciet beperken met --workers=4 om overeen te komen met onze werkelijke toewijzing.
We ontdekten ook dat hogere parallelisme racecondities blootlegde in onze testopruimingslogica. Integratietests die sequentieel prima werkten, zouden botsen bij gelijktijdige uitvoering, wat idempotencyKey fouten produceerde. De oplossing vereiste een heroverweging van hoe tests zichzelf opschonen: boekingen opvragen op eventTypeId in plaats van individuele boekings-IDs bij te houden, die boekingen miste die indirect door de testcode waren aangemaakt (#26269, #26278, #26283).
Zelfs onze screenshottest moest worden aangepast (#26247). Verschillende runners laten subtiele verschillen in lettertypeweergave zien, dus hebben we deviceScaleFactor: 1 toegevoegd voor consistente weergave en onze diffdrempel iets verhoogd van 0.05 naar 0.07 om rekening te houden met antialiasingvariaties zonder echte regressies te maskeren.
Sneller bewegen legt vaak verborgen koppelingen in je systeem bloot. De koppeling was er altijd; je kon het alleen niet zien bij lagere snelheden. Het oplossen van deze problemen maakt duurzame snelheidsverbeteringen mogelijk.
Principe 1: Verkort het Kritieke Pad met Afhankelijkheidsontwerp
Voor ons was een groot probleem dat lint dingen blokkeerde die het niet zou moeten blokkeren. Wanneer je E2E-tests 7-10 minuten duren, maakt het de hele workflow langer om ze te blokkeren op 2-3 minuten lint- en typechecks. De lint zal veel eerder klaar zijn dan E2E, dus waarom ze niet parallel laten draaien?
Ontkoppel niet-gerelateerde test suites
Onze integratietests en de belangrijkste E2E suite hebben de API v2 build niet nodig, dus hebben we die afhankelijkheid verwijderd (#26170). Omgekeerd hebben de API v2 E2E tests de belangrijkste webbuild niet nodig. Elke test suite hangt nu alleen af van wat het daadwerkelijk vereist, wat maximale parallelisme mogelijk maakt.
Consolideer preflight jobs
We hadden aparte jobs voor bestandswijzigingsdetectie (changes) en E2E labelcontroles (check-label). Elke job heeft opstartoverhead: een runner opstarten, code inchecken, context herstellen. Door deze samen te voegen in een enkele prepare job, hebben we overbodige overhead geëlimineerd (#26101). We zijn verder gegaan en hebben onze afhankelijkheidsinstallatiestappen ook in de prepare job geplaatst, waardoor we nog eens 20 seconden per workflow bespaarden door opnieuw een jobgrens te elimineren (#26320).
Maak rapportage non-blocking
E2E testverslagen zijn waardevol, maar ze zouden niet op het kritieke pad moeten staan. We hebben de merge-reports, publish-report en cleanup-report jobs verplaatst naar een aparte workflow die wordt geactiveerd door workflow_run (#26157). Nu kunnen engineers falende jobs onmiddellijk opnieuw uitvoeren zonder te wachten op de voltooiing van de rapportgeneratie.
De regel is eenvoudig: laat E2E-jobs eerder starten. Blokkeer lange taken niet op kortlopende vereisten, tenzij er een echte afhankelijkheid is. De tijd van je CI wordt bepaald door het kritieke pad, en elke onnodige afhankelijkheid verlengt dat pad.
Principe 2: Caching Is Koning (Maar Alleen Als Het Klopt)
Effectieve caching vereist sleutels die ongeldig worden wanneer ze dat moeten, stabiel blijven wanneer ze dat moeten, en een cache-opslag die in de loop van de tijd gezond blijft.
Maak gebruik van lookup-only
Onze meest dramatische cachingverbetering kwam voort uit een eenvoudige observatie: we downloadden ongeveer 1,2 GB aan gecachete gegevens, zelfs wanneer we niets hoefden te installeren. De cache bestond, de hit was succesvol, maar we betaalden nog steeds de downloadkosten voordat we ontdekten dat we de installatiestap konden overslaan.
De oplossing was om lookup-only cachecontroles te gebruiken (#26314). We hebben speciale stappen die voor alle jobs draaien om ervoor te zorgen dat afhankelijkheden zijn geïnstalleerd, zodat we weten dat de cache bestaat tegen de tijd dat individuele jobs draaien. Voordat we proberen om de volledige cache te herstellen, controleren we eerst of het bestaat zonder te downloaden. Als alle caches weergeven, slaan we de hele herstel- en installatiestroom over. Geen downloads, geen installaties, gewoon doorgaan met het daadwerkelijke werk. Dit alleen al bespaarde aanzienlijke tijd bij elke cache-hitscenario, wat, met een goed cache-sleutelontwerp, de meeste uitvoeringen zou moeten zijn.
We pasten dezelfde logica toe op de installatie van de Playwright-browser (#26060). Aangezien we een speciale stap hebben die ervoor zorgt dat Playwright-browsers zijn gecached voordat testjobs draaien, kunnen we dezelfde lookup-only benadering gebruiken. Als de Playwright-cache het haalt, sla de installatie volledig over. Nu controleren we eerst, en beslissen dan of we überhaupt moeten downloaden.
We breidden dit patroon ook uit naar onze databasecache (#26344). Met behulp van lookup-onlycontroles kunnen we het opnieuw opbouwen van de DB-cache overslaan wanneer deze al bestaat. We vereenvoudigden ook de cachesleutel door het PR-nummer en de SHA te verwijderen, waardoor het veel herbruikbaarder wordt over uitvoeringen.
Cachecorrectheid
Cachecorrectheid is net zo belangrijk als cachesnelheid. We ontdekten dat ons **/node_modules/ glob patroon apps/web/.next/node_modules/ vastlegde, wat de buildoutput van Next.js is met symlinks en gegenereerde bestanden die veranderen tijdens builds. Dit leidde tot corrupte cache-archieven met tar-extractiefouten (#26268). De oplossing was een eenvoudig uitsluitingspatroon, maar het vinden ervan vereiste een goed begrip van wat precies werd gecached en waarom.
We implementeerden ook automatische cacheopruiming (#26312). Wanneer een PR wordt samengevoegd of gesloten, verwijderen we nu de bijbehorende build caches. Dit houdt onze cache-opslag slank en voorkomt de accumulatie van verouderde invoer die nooit meer gebruikt zou worden. We vereenvoudigden ook ons cachesleutelformaat in het proces, waardoor onnodige segmenten zoals runner.os en node_version werden verwijderd, wat de complexiteit zonder het verbeteren van hitpercentages verhoogde.
Turbo Remote Caching
We upgradeden naar Next.js 16 (#26093), wat onze builds van meer dan 6 minuten reduceerde naar ~1 minuut (shoutout naar het Next.js team voor enorme verbeteringen). Met Turbo Remote Caching ingeschakeld, kosten volgende builds slechts 7 seconden om uit de cache te trekken vergeleken met de ~1 minuut voor een volledige build.
We hebben ook Turbo Remote Caching ingeschakeld voor onze API v2 E2E tests (#26331). Voorheen zou elke E2E shard de platformpakketten helemaal opnieuw bouwen. Nu profiteert elke shard van de remote cache van Turbo, en we hebben de TypeScript-compilatie van Jest geoptimaliseerd door isolatedModules in te schakelen en diagnostiek in CI uit te schakelen, wat de opstarttijd van tests aanzienlijk versnelt.
Principe 3: Tests Schalen met Sharding
Wanneer je meer dan 1.100 testgevallen hebt verspreid over 82 E2E testbestanden, laat je de prestaties liggen als je ze sequentieel uitvoert. Testsharding, waarbij je je test suite over meerdere parallelle runners splitst, is de meest directe manier om de wandklok tijd voor grote test suites te reduceren. We hadden al sharding in onze belangrijkste E2E suite, maar we hebben deze verhoogd van 4 naar 8 shards (#26342), wat de totale tijd van de E2E suite met nog een minuut verlaagde. Onze API v2 E2E tests draaiden nog steeds als een enkele job.
We shardden onze API v2 E2E tests in 4 parallelle jobs met behulp van de ingebouwde --shard optie van Jest (#26183). Elke shard draait onafhankelijk met zijn eigen Postgres- en Redis-diensten, en artifacts zijn uniek per shard benoemd. Wat voorheen meer dan 10 minuten duurde als een enkele job, wordt nu aanzienlijk sneller voltooid met het werk verdeeld over vier runners.
Sharding op grote schaal legt ook infrastructuurproblemen bloot die je misschien niet opmerkt bij sequentiële uitvoering. Wanneer meerdere jobs tegelijkertijd proberen de dezelfde databasecache in te vullen, krijg je racecondities. We losten dit op door een speciale setup-db job te creëren die vóór alle E2E- en integratietestjobs draait (#26171). Deze enkele job vult de databasecache één keer, en alle downstream jobs herstellen deze. Geen races, geen dubbele werkzaamheden.
We creëerden ook een speciale workflow voor API v2 unit tests, waardoor ze gescheiden zijn van de belangrijkste test suite (#26189). Dit stelt ze in staat om parallel te draaien met andere controles in plaats van te concurreren om resources in een monolithische testjob.
Het Samengestelde Effect
Geen van deze veranderingen op zichzelf zou onze CI-tijd met 83% hebben verminderd. De winsten komen van hun combinatie.
Slimme afhankelijkheden laten jobs eerder beginnen. Effectieve caching betekent dat die jobs minder tijd besteden aan opzetten. Sharding betekent dat de daadwerkelijke testuitvoering parallel gebeurt. Samen comprimeren ze het kritieke pad vanuit elke richting.
De investering wordt in de loop van de tijd groter. Elke PR krijgt nu sneller feedback. Engineers blijven langer in de flow. De verleiding om wijzigingen te bundelen neemt af, wat betekent dat er kleinere, beter reviewbare diffs zijn. De codekwaliteit verbetert omdat de feedbackloop strakker is.
We zullen blijven investeren in CI-prestaties omdat de opbrengsten echt en meetbaar zijn. Het doel is niet snelheid om de snelheid, maar om engineers in staat te stellen kwalitatief hoogwaardige software sneller te leveren. Wanneer CI snel is, werken snel verplaatsen en hoge normen samen in plaats van tegen elkaar in.
Verwijzingen naar Pull Requests
Basis: Migratie van Buildjet naar Blacksmith
#26247 - Migreer GitHub-workflows van Buildjet naar Blacksmith
Teststabiliteit
#26269 - Fix onbetrouwbare e2e-tests met geïsoleerde gebruikerssessies
#26278 - Stabiliseer e2e-tests met gescopeerde locators en deterministische planningen
#26283 - Verbeter testisolatie voor beheerde eventtype e2e-tests
Turbo Remote Caching
#26093 - Upgrade naar Next 16 (builds gereduceerd van meer dan 6 minuten tot ~1 minuut)
#26331 - Schakel Turbo Remote Caching in (volgende builds duren 7s)
Jobafhankelijkheden en Workflowoptimalisatie
#26101 - Consolideer veranderingen en check-label jobs in prepare
#26170 - Ontkoppel niet-API v2 tests van API v2 build
#26157 - Maak E2E-rapportjobs non-blocking door ze naar een aparte workflow te verplaatsen
#26320 - Verplaats afhankelijkheidsjob naar prepare job-stap om ~20s per workflow te besparen
Maak gebruik van lookup-only
#26314 - Gebruik lookup-only cachecheck om downloads van afhankelijkheidsjobs over te slaan
#26060 - Sla Playwright-installatie over bij cache-hit
#26344 - Gebruik lookup-only voor DB-cache en vereenvoudig cachesleutel
Cachecorrectheid
#26268 - Sluit .next/node_modules uit van yarn-cache om corruptie te voorkomen
#26312 - Verwijder cache-build cache-invoer bij PR-sluiting
Testsharding en Infrastructuur

Begin vandaag nog gratis met Cal.com!
Ervaar naadloze planning en productiviteit zonder verborgen kosten. Meld je in enkele seconden aan en begin vandaag nog met het vereenvoudigen van je planning, geen creditcard vereist!


