Guide technique

Automatisation de Navigateur IA Sans BrowserBase : Ce Que Nous Avons Construit à la Place

Comment construire une automatisation de navigateur avec Playwright et des LLM au lieu d'outils payants. Pool d'instances, gestion de sessions, compréhension de page par LLM et comparaison de coûts.

16 février 202614 min de lectureÉquipe d'Ingénierie Oronts

Le marché des outils d'automatisation de navigateur payants

BrowserBase, Browserless et les services similaires facturent à la minute ou à la session pour des navigateurs headless managés. Pour les workflows IA qui doivent interagir avec des pages web (remplir des formulaires, extraire des données structurées, naviguer dans des processus multi-étapes), ces services gèrent l'infrastructure : instances de navigateur, anti-détection, proxies et gestion de sessions.

Les coûts s'accumulent vite. À 0,10-0,50 $ par minute de session, un workflow qui traite 1 000 pages par jour à 2 minutes chacune coûte 200-1 000 $ par jour. Pour un système IA qui tourne en continu, ça fait 6 000-30 000 $ par mois juste pour l'infrastructure navigateur.

Nous avons construit une alternative auto-hébergée avec Playwright + LLM pour la compréhension de page. Elle couvre 90 % des cas d'usage à une fraction du coût. Cet article couvre l'architecture. Pour comprendre comment nous construisons des systèmes de workflows IA et de l'IA agentique de manière plus large, ces guides couvrent les patterns de niveau supérieur.

L'architecture

┌─────────────────────────────────────────────────────────┐
│                  AI Browser Engine                       │
│                                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  Task Queue   │  │  Instance    │  │  Session      │  │
│  │  (BullMQ)     │  │  Pool        │  │  Manager      │  │
│  │               │  │  (Playwright │  │  (cookies,    │  │
│  │  Prioritized  │  │   browsers)  │  │   localStorage│  │
│  │  Retry logic  │  │              │  │   auth state) │  │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  │
│         │                 │                  │           │
│         ▼                 ▼                  ▼           │
│  ┌──────────────────────────────────────────────────┐   │
│  │              Page Interaction Layer                │   │
│  │                                                    │   │
│  │  1. Naviguer vers l'URL                           │   │
│  │  2. Attendre le chargement de la page             │   │
│  │  3. Extraire la structure (arbre d'accessibilité) │   │
│  │  4. Envoyer la structure au LLM                   │   │
│  │  5. Le LLM retourne un plan d'action              │   │
│  │  6. Exécuter les actions via Playwright           │   │
│  │  7. Extraire les données structurées du résultat  │   │
│  └──────────────────────────────────────────────────┘   │
│                                                          │
└─────────────────────────────────────────────────────────┘

Pool d'instances

Lancer un nouveau navigateur pour chaque tâche coûte cher (démarrage à froid : 1-3 secondes, mémoire : 200-400 Mo par instance). Un pool réutilise les instances de navigateur entre les tâches.

class BrowserPool {
    private available: Browser[] = [];
    private inUse = new Map<string, Browser>();
    private maxInstances: number;

    constructor(options: { maxInstances: number }) {
        this.maxInstances = options.maxInstances;
    }

    async acquire(): Promise<{ browser: Browser; id: string }> {
        // Réutiliser une instance disponible
        if (this.available.length > 0) {
            const browser = this.available.pop()!;
            const id = crypto.randomUUID();
            this.inUse.set(id, browser);
            return { browser, id };
        }

        // Créer une nouvelle si on est sous la limite
        if (this.inUse.size < this.maxInstances) {
            const browser = await chromium.launch({
                headless: true,
                args: [
                    '--no-sandbox',
                    '--disable-setuid-sandbox',
                    '--disable-dev-shm-usage',
                    '--disable-gpu',
                    '--single-process',
                ],
            });
            const id = crypto.randomUUID();
            this.inUse.set(id, browser);
            return { browser, id };
        }

        // Pool épuisé : attendre qu'une instance se libère
        return new Promise((resolve) => {
            this.waitQueue.push(resolve);
        });
    }

    async release(id: string): Promise<void> {
        const browser = this.inUse.get(id);
        if (!browser) return;

        this.inUse.delete(id);

        // Nettoyer l'état entre les tâches
        const pages = browser.contexts();
        for (const context of pages) {
            await context.close();
        }

        // Si quelqu'un attend, lui donner cette instance
        if (this.waitQueue.length > 0) {
            const resolve = this.waitQueue.shift()!;
            const newId = crypto.randomUUID();
            this.inUse.set(newId, browser);
            resolve({ browser, id: newId });
        } else {
            this.available.push(browser);
        }
    }
}

Dimensionnement du pool

Charge de travailTaille du poolMémoire requise
Légère (< 100 pages/heure)2-3 instances1-2 Go
Moyenne (100-500 pages/heure)5-10 instances3-5 Go
Lourde (500+ pages/heure)10-20 instances5-10 Go

Chaque instance Chromium consomme 200-400 Mo de RAM. La taille du pool détermine ton plafond de débit et tes besoins en mémoire. Commence petit et ajuste en fonction de la charge réelle.

Gestion de sessions

Beaucoup de workflows nécessitent de maintenir l'état de connexion entre plusieurs interactions avec les pages. Le gestionnaire de sessions persiste les cookies, le localStorage et les tokens d'authentification entre les tâches.

class SessionManager {
    private sessions = new Map<string, SessionState>();

    async createSession(id: string, options: SessionOptions): Promise<BrowserContext> {
        const context = await browser.newContext({
            viewport: { width: 1280, height: 720 },
            userAgent: options.userAgent || this.getRandomUserAgent(),
            locale: options.locale || 'en-US',
            timezoneId: options.timezone || 'Europe/Berlin',
        });

        // Restaurer l'état de session précédent s'il existe
        const existing = this.sessions.get(id);
        if (existing) {
            await context.addCookies(existing.cookies);
            // localStorage restauré via page.evaluate après navigation
        }

        return context;
    }

    async saveSession(id: string, context: BrowserContext): Promise<void> {
        const cookies = await context.cookies();
        const pages = context.pages();
        let localStorage = {};

        if (pages.length > 0) {
            localStorage = await pages[0].evaluate(() => {
                const data: Record<string, string> = {};
                for (let i = 0; i < window.localStorage.length; i++) {
                    const key = window.localStorage.key(i);
                    if (key) data[key] = window.localStorage.getItem(key) || '';
                }
                return data;
            });
        }

        this.sessions.set(id, {
            cookies,
            localStorage,
            lastUsed: Date.now(),
        });
    }
}

Compréhension de page pilotée par LLM

L'innovation principale : au lieu d'écrire des sélecteurs CSS ou des requêtes XPath pour chaque page, on envoie l'arbre d'accessibilité de la page à un LLM et on le laisse décider avec quels éléments interagir.

async function extractPageStructure(page: Page): Promise<string> {
    // Récupérer l'arbre d'accessibilité (représentation structurée et compacte)
    const tree = await page.accessibility.snapshot();

    // Convertir en format texte compréhensible par le LLM
    return formatAccessibilityTree(tree, {
        maxDepth: 5,
        includeRoles: ['button', 'link', 'textbox', 'combobox', 'checkbox', 'heading'],
        includeText: true,
        includeLabels: true,
    });
}

function formatAccessibilityTree(node: any, options: any, depth = 0): string {
    if (depth > options.maxDepth) return '';
    if (!options.includeRoles.includes(node.role) && depth > 1) {
        // Ignorer les éléments non interactifs, mais parcourir les enfants
        return (node.children || []).map(c => formatAccessibilityTree(c, options, depth + 1)).join('');
    }

    const indent = '  '.repeat(depth);
    let result = `${indent}[${node.role}] ${node.name || ''}`;
    if (node.value) result += ` value="${node.value}"`;
    result += '\n';

    for (const child of node.children || []) {
        result += formatAccessibilityTree(child, options, depth + 1);
    }
    return result;
}

Planification d'actions par le LLM

On envoie la structure de la page au LLM avec la description de la tâche. Le LLM retourne une séquence d'actions :

async function planActions(pageStructure: string, task: string): Promise<Action[]> {
    const response = await llm.generate({
        model: 'gpt-4o-mini', // Modèle rapide pour la planification d'actions
        messages: [
            {
                role: 'system',
                content: `You are a browser automation assistant. Given a page structure and a task,
                return a JSON array of actions to accomplish the task.
                Available actions: click(selector), type(selector, text), select(selector, value),
                wait(ms), extract(selector).
                Use the element text/labels to identify targets, not CSS selectors.`,
            },
            {
                role: 'user',
                content: `Page structure:\n${pageStructure}\n\nTask: ${task}`,
            },
        ],
        responseFormat: 'json',
    });

    return JSON.parse(response.text);
}

// Exemple de tâche : "Remplir le formulaire de contact avec le nom Sara Mustermann et l'email sara.mustermann@beispiel.de"
// Le LLM retourne :
// [
//   { "action": "type", "target": "Name input field", "value": "Sara Mustermann" },
//   { "action": "type", "target": "Email input field", "value": "sara.mustermann@beispiel.de" },
//   { "action": "click", "target": "Submit button" }
// ]

Résolution des actions LLM en commandes Playwright

Le LLM retourne des cibles lisibles par un humain ("Name input field"). Un résolveur les mappe vers des sélecteurs Playwright :

async function resolveAndExecute(page: Page, actions: Action[]): Promise<void> {
    for (const action of actions) {
        // Trouver l'élément correspondant à la description du LLM
        const element = await findElementByDescription(page, action.target);

        if (!element) {
            throw new ActionError(`Could not find element: ${action.target}`);
        }

        switch (action.action) {
            case 'click':
                await element.click();
                await page.waitForLoadState('networkidle');
                break;
            case 'type':
                await element.fill(action.value);
                break;
            case 'select':
                await element.selectOption(action.value);
                break;
            case 'wait':
                await page.waitForTimeout(action.value);
                break;
            case 'extract':
                const text = await element.textContent();
                results.push({ field: action.target, value: text });
                break;
        }
    }
}

async function findElementByDescription(page: Page, description: string): Promise<ElementHandle | null> {
    // Essayer plusieurs stratégies pour trouver l'élément
    const strategies = [
        // Par aria-label
        () => page.$(`[aria-label*="${description}" i]`),
        // Par placeholder
        () => page.$(`[placeholder*="${description}" i]`),
        // Par texte visible
        () => page.$(`text=${description}`),
        // Par association de label
        () => page.$(`label:has-text("${description}") + input, label:has-text("${description}") input`),
        // Par rôle et nom
        () => page.getByRole('textbox', { name: new RegExp(description, 'i') }).first().elementHandle(),
        () => page.getByRole('button', { name: new RegExp(description, 'i') }).first().elementHandle(),
    ];

    for (const strategy of strategies) {
        try {
            const element = await strategy();
            if (element) return element;
        } catch {
            continue;
        }
    }

    return null;
}

Anti-détection : les bases

Certains sites web détectent et bloquent les navigateurs headless. Les contre-mesures de base :

const context = await browser.newContext({
    // Randomiser le viewport
    viewport: {
        width: 1280 + Math.floor(Math.random() * 200),
        height: 720 + Math.floor(Math.random() * 100),
    },

    // Rotation des user agents
    userAgent: getRandomUserAgent(),

    // Locale et fuseau horaire réalistes
    locale: 'de-DE',
    timezoneId: 'Europe/Berlin',

    // Géolocalisation réaliste
    geolocation: { latitude: 48.1351, longitude: 11.5820 },
    permissions: ['geolocation'],
});

// Surcharger navigator.webdriver (détection de headless)
await page.addInitScript(() => {
    Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
});

Note : l'anti-détection est une course aux armements. Pour les sites avec une détection de bots sophistiquée (Cloudflare, Akamai), le Playwright auto-hébergé finira par être détecté. C'est là que les services payants comme BrowserBase apportent de la valeur : ils investissent continuellement dans l'anti-détection. Pour la plupart des tâches d'automatisation métier (outils internes, portails partenaires, données publiques), l'anti-détection de base suffit.

Quand les outils payants VALENT le coût

ScénarioAuto-hébergéService payant
Automatisation d'outils internesMeilleur choix (pas d'anti-détection nécessaire)Surdimensionné
Extraction de données publiques (simple)Bon (l'anti-détection basique fonctionne)Inutile
Sites avec détection de botsPossible mais maintenance constanteJustifié (ils gèrent l'anti-détection)
Scraping à haut volume (10K+ pages/jour)Complexe (rotation de proxy, gestion d'IP)Justifié (infrastructure managée)
Données réglementées (RGPD, conformité)Mieux (les données restent sur ton infrastructure)Risque (les données passent par un tiers)
Migration ponctuelleBon (charge de travail temporaire)Coût inutile

Le cadre de décision : si tu automatises des workflows internes ou traites des données publiques de sites sans détection de bots agressive, auto-héberge. Si tu fais de l'extraction à haut volume depuis des sites avec une protection niveau Cloudflare, paie un service dont l'anti-détection est le coeur de métier.

Comparaison de coûts

ComposantAuto-hébergé (mensuel)BrowserBase (mensuel)
Compute (5 instances)50-100 $ (conteneur/VPS)N/A
Appels LLM (planification d'actions)20-50 $ (GPT-4o-mini)N/A
Sessions BrowserBaseN/A500-2 000 $
Service de proxy (si nécessaire)50-200 $Inclus
Maintenance2-4 heures/moisAucune
Total (1 000 pages/jour)120-350 $/mois500-2 000 $/mois
Total (10 000 pages/jour)300-800 $/mois3 000-10 000 $/mois

L'auto-hébergement est 3 à 10 fois moins cher à l'échelle. La contrepartie, c'est le temps de maintenance et les capacités d'anti-détection.

Erreurs courantes

  1. Pas de pool d'instances. Lancer un nouveau navigateur par tâche gaspille 1-3 secondes en démarrage à froid et 200-400 Mo de RAM. Mets les instances en pool et réutilise-les.

  2. Sélecteurs CSS en dur. Les pages modifient régulièrement leur structure DOM. L'identification d'éléments par LLM est plus résiliente que des sélecteurs en dur.

  3. Pas de persistance de session. Les workflows multi-étapes qui nécessitent une connexion échouent quand l'état de session est perdu entre les étapes.

  4. Ignorer complètement l'anti-détection. Même les mesures de base (viewport aléatoire, rotation de user agent, surcharge de webdriver) empêchent la détection sur la plupart des sites.

  5. Utiliser un gros modèle pour la planification d'actions. GPT-4o-mini ou Claude Haiku sont assez rapides pour la compréhension de page. Un gros modèle ajoute de la latence sans améliorer la précision pour cette tâche.

  6. Pas de timeout sur le chargement des pages. Certaines pages chargent indéfiniment (scroll infini, scripts tiers lents). Définis un timeout de navigation et gère-le.

  7. Tourner en production sans monitoring. Suis le taux de succès, le temps d'exécution moyen et les types d'erreurs par workflow. Alerte quand le taux de succès chute.

Points clés à retenir

  • Playwright auto-hébergé + LLM couvre 90 % des cas d'automatisation de navigateur. Pour les outils internes, portails partenaires et données publiques sans détection de bots agressive, c'est la bonne approche.

  • Le pool d'instances est essentiel. Réutilise les instances de navigateur entre les tâches. Les démarrages à froid et l'allocation mémoire sont le plus gros goulot d'étranglement.

  • La compréhension de page par LLM remplace les sélecteurs fragiles. Envoie l'arbre d'accessibilité à un modèle rapide. Laisse-le décider avec quels éléments interagir. Plus résilient aux changements de page que des sélecteurs CSS en dur.

  • Les services payants méritent leur coût pour l'anti-détection. Si tes sites cibles ont Cloudflare ou une protection similaire, BrowserBase investit continuellement pour la contourner. C'est leur coeur de métier. N'essaie pas de rivaliser.

  • L'auto-hébergement est 3 à 10 fois moins cher à l'échelle. Mais tu paies en temps de maintenance et en limitations d'anti-détection. Fais le compromis en connaissance de cause.

Nous intégrons l'automatisation de navigateur dans nos systèmes de workflows IA et nos projets de logiciels sur mesure. Si tu as besoin d'aide pour l'architecture d'automatisation de navigateur, parle à notre équipe ou demande un devis.

Sujets couverts

automatisation navigateur IAnavigateur headless TypeScriptPlaywright IAautomatisation navigateur open sourcealternative BrowserBaseLLM navigateurscraping web IA

Prêt à construire des systèmes IA prêts pour la production ?

Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.

Démarrer une conversation