
Je suis Mathieu Ponton, Co-Founder & ingénieur logiciel chez Apogée Consult à Lyon. Ingénieur diplômé de Polytech Lyon (Informatique), j'ai fait trois ans en apprentissage, partagés entre la Métropole de Lyon (inclusion numérique avec Res'in et sobriété énergétique avec Écolyo) et Superwyze, une startup medtech (POCs, dont certains aujourd'hui industrialisés, et travail sur des codebases existantes). J'ai livré plus de 10 projets en production (web, mobile et IA / RAG) pour des PME, startups et organisations publiques.
CI sous 3 minutes : ce qu'on a coupé et ce qu'on a accepté de perdre
- devops
Notre CI Next.js mettait 12 minutes. Elle en prend maintenant 2 minutes 40. Voici exactement ce qu'on a supprimé, ce qu'on a parallélisé, et les risques qu'on a consciemment acceptés.
CI sous 3 minutes : ce qu'on a coupé et ce qu'on a accepté de perdre
Une CI à 12 minutes, c'est une CI qu'on finit par ignorer. On ouvre autre chose, on perd le contexte, on oublie de vérifier le résultat. Sur notre projet principal, nous avons réduit le temps de CI de 12 minutes à 2 minutes 40 en quatre semaines. Voici le détail des décisions, y compris celles où on a délibérément accepté un risque.
L'état de départ : 12 minutes pour quoi
Avant optimisation, notre workflow GitHub Actions enchaînait :
- Checkout + install npm, 2 min 30
- Typecheck TypeScript, 3 min 10
- Lint ESLint, 2 min 20
- Tests unitaires Jest, 2 min 50
- Build Next.js, 4 min 10
- (optionnel) Tests E2E Playwright, 8 min
Total sans E2E : 15 min. Avec E2E sur chaque PR : 23 min.
Tout tournait séquentiellement dans un seul job. C'est le premier problème.
Ce qu'on a fait
1. Cache npm avec hash du lockfile
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ciactions/setup-node avec cache: 'npm' met en cache ~/.npm basé sur le hash de package-lock.json. Si le lockfile n'a pas changé, npm ci devient essentiellement un dézip de cache plutôt qu'un téléchargement réseau.
Gain mesuré : de 2 min 30 à 35 secondes sur un cache chaud.
2. Parallélisation des jobs
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'npm' }
- run: npm ci
- run: npm run typecheck
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'npm' }
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'npm' }
- run: npm ci
- run: npm run test -- --passWithNoTests
build:
runs-on: ubuntu-latest
needs: [typecheck, lint, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'npm' }
- run: npm ci
- run: npm run buildTypecheck, lint, et tests tournent en parallèle. Le build attend leur succès. Chaque job repaie le coût d'install npm (35 secondes sur cache chaud), mais les trois jobs les plus longs s'exécutent simultanément.
Gain mesuré après parallélisation : de 15 min à 5 min 20.
3. Cache du build Next.js
build:
runs-on: ubuntu-latest
needs: [typecheck, lint, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'npm' }
- run: npm ci
- name: Cache Next.js build
uses: actions/cache@v4
with:
path: |
.next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('**/*.ts', '**/*.tsx', '**/*.css') }}
restore-keys: |
nextjs-${{ runner.os }}-
- run: npm run buildNext.js maintient un cache de compilation dans .next/cache. Si les fichiers sources n'ont pas changé, les modules déjà compilés sont réutilisés. Sur une PR qui ne touche que quelques composants, une grande partie du build est servie depuis le cache.
Gain mesuré sur une PR typique (5-10 fichiers modifiés) : de 4 min 10 à 1 min 50.
Ce qu'on a supprimé
Les tests E2E sur chaque PR
Les tests Playwright prenaient 8 minutes. Nous les avons déplacés sur le merge dans main uniquement, pas sur les PR.
C'est le compromis le plus significatif. Un bug d'intégration UI peut maintenant atterrir sur main avant d'être détecté. Notre mitigation : des tests E2E qui bloquent le déploiement sur staging, pas seulement sur main. Si les E2E cassent sur staging, le déploiement en production est bloqué.
On a perdu la détection précoce sur PR. On a gagné 8 minutes sur chaque PR de l'équipe.
Le lint CSS Stylelint
Nous avions Stylelint pour les fichiers CSS. Depuis la migration vers Tailwind v4 (classes utilitaires uniquement, peu de CSS custom), Stylelint ne détectait plus grand-chose d'utile. Nous l'avons supprimé.
Gain : 45 secondes. Risque : quasi nul sur notre codebase actuelle.
Les imports vérifiés deux fois
ESLint avec eslint-plugin-import vérifiait les imports. TypeScript avec noUnusedLocals et moduleResolution: bundler vérifiait aussi les imports. Il y avait du recouvrement. Nous avons simplifié la config ESLint pour supprimer les règles d'import déjà couvertes par TypeScript.
Ce qu'on a refusé de couper
Le typecheck complet
Certains conseillent de désactiver le typecheck en CI et de se fier uniquement à TypeScript dans l'éditeur. C'est risqué. Les développeurs ont des configurations locales variées, des plugins VS Code qui ignorent certaines erreurs, et des branches locales avec des types temporairement cassés. Le typecheck CI est la seule garantie réelle.
Les tests unitaires sur les fonctions métier critiques
Nous avons réduit le scope des tests : moins de tests de snapshot, plus de tests sur la logique métier pure. Mais nous n'avons pas supprimé les tests. Un régresssion sur le calcul des prix ou la logique d'autorisation coûte plus cher à corriger en production que 1 minute de CI.
Le build Next.js
Certains CI skippent le build et font confiance au typecheck. Mais le build détecte des erreurs que le typecheck ne voit pas : dynamic imports mal formés, erreurs de configuration des Server Components, problèmes de bundling spécifiques à Webpack/Turbopack. Sur deux occasions, le build a détecté un bug que le typecheck avait raté.
Le résultat
| Étape | Avant | Après |
|---|---|---|
| Install npm | 2 min 30 | 35 s (× 3 jobs parallèles) |
| Typecheck | 3 min 10 | 2 min 45 (cache tsc) |
| Lint | 2 min 20 | 1 min 50 |
| Tests | 2 min 50 | 2 min 10 |
| Build | 4 min 10 | 1 min 50 (cache .next) |
| Total mur | 15 min | 2 min 40 |
Les trois jobs parallèles (typecheck, lint, test) prennent chacun ~2 min 45 au maximum. Le build attend et prend 1 min 50. Total sur le chemin critique : 2 min 40.
La limite du cache GitHub Actions
Le cache GitHub Actions a une limite de 10 Go par repository. Si vous avez beaucoup de branches actives, les caches s'évincent mutuellement. Sur un repo avec 20 PRs ouvertes en parallèle, le cache du build Next.js peut ne pas survivre entre deux pushes sur la même branche.
Nous complétons avec le cache Turborepo remote si le projet utilise un monorepo, mais pour un projet single-repo, le cache GitHub Actions standard suffit.
La question qu'on ne pose pas assez : quelle est la valeur d'une CI à 3 minutes vs 12 minutes pour votre équipe ? Si chaque développeur fait 5 pushes par jour, passer de 12 à 3 minutes libère 45 minutes/développeur/jour, ou permet de ne plus ignorer les résultats en attente.