Apogée Consult
Retour au blog
Mathieu Ponton
Mathieu PontonCo-Founder & ingénieur logiciel

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 :

  1. Checkout + install npm, 2 min 30
  2. Typecheck TypeScript, 3 min 10
  3. Lint ESLint, 2 min 20
  4. Tests unitaires Jest, 2 min 50
  5. Build Next.js, 4 min 10
  6. (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 ci

actions/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 build

Typecheck, 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 build

Next.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

ÉtapeAvantAprès
Install npm2 min 3035 s (× 3 jobs parallèles)
Typecheck3 min 102 min 45 (cache tsc)
Lint2 min 201 min 50
Tests2 min 502 min 10
Build4 min 101 min 50 (cache .next)
Total mur15 min2 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.

Disponible pour de nouveaux projets

Un projet à concrétiser ?
Parlons-en, sans engagement.

Un échange de 30 minutes pour cadrer votre besoin, qualifier la faisabilité et vous proposer une trajectoire claire.

1// kick-off : réponse sous 24h
2const project = await apogee.scope({
3 type: 'web | mobile | IA',
4 timeline: '6 à 16 semaines',
5 approach: 'sur-mesure'
6})
7// → cadrage offert
CI rapide Next.js : passer sous 3 minutes | Apogée Consult