Open source — Obscura

Navigateur headless pour agents IA en Rust : vu de l'amont

Ce que j'ai appris en contribuant en amont à Obscura, le navigateur headless en Rust que j'utilise en production sur asap.cool.

Mon rôle : Contributeur.

Le problème

Le 16 juillet 2026, j’ouvre ma première issue sur Obscura. Sur asap.cool, le module sourcing collecte des fiches Google Maps pour alimenter les leads, et je venais de faire tourner plusieurs connexions CDP en parallèle sur le même moteur. Le processus s’arrêtait net avec un abort V8 dès que deux navigations se chevauchaient, pas une erreur mais un arrêt sec.

Pour comprendre pourquoi j’étais là plutôt que sur Chrome headless : quand tu pilotes Chrome avec Puppeteer ou Playwright, chaque session concurrente embarque un Chromium complet. Pour un agent qui ouvre une page de temps en temps, ça passe. Pour une collecte à plusieurs sessions en continu, le moteur devient le poste de coût principal, et c’est ce que je voulais éviter.

Obscura et Lightpanda attaquent ce point de la même façon : un moteur natif, sans Chromium, qui parle CDP pour que ton code Puppeteer ou Playwright ne change pas. Obscura est en Rust, Lightpanda en Zig. Ce que je peux raconter, c’est ce que ça demande d’en utiliser un en production quand il lui manque un morceau.

Le réflexe facile, c’est de patcher dans son coin, et ça marche un mois. Le truc, c’est qu’un moteur qui s’écarte du comportement de Chrome casse silencieusement tout ce qui a été écrit pour Chrome, à commencer par les pages que tu scrapes. Alors j’ai choisi de corriger en amont, avec des tests, et de garder un fork qui suit main plutôt qu’un fork qui diverge.

L’approche

Obscura (h4ckf0r0day/obscura, Apache-2.0, 27 427 étoiles au 20 septembre 2026) est un navigateur headless écrit en Rust, sans dépendance à Chromium, qui exécute du JavaScript réel via V8 grâce à deno_core et implémente le Chrome DevTools Protocol. Je suis contributeur, pas mainteneur : le mainteneur décide de ce qui entre.

Entre le 16 juillet et le 3 août 2026, j’ai ouvert 6 issues et 5 PR en amont, dont 3 mergées entre le 19 et le 24 juillet, et poussé 33 commits sur le fork asap-cool/obscura.

Ce que contribuer en amont a demandéQuatre étapes, du bug observé en prod au fork qui suit l'amontReproduirevérifier contre Chrome headless réelCorriger en amontpas dans son coinLe forkqui suit l’amontLa concurrence CDPet le benchmark

Reproduire et vérifier contre Chrome headless réel

Le bug qui m’a le plus appris est petit. Sur Obscura, assigner el.scrollTop = 100 déplaçait bien le contenu, mais ne déclenchait aucun événement scroll, alors que scrollTo et scrollBy le faisaient. Un lazy-loader qui écoute scroll pour charger la suite d’une liste ne se réveillait donc jamais. Exactement le genre de chose qu’un scraper Google Maps rencontre à chaque page.

Avant de proposer quoi que ce soit, j’ai vérifié ce que fait Chrome, pas ce que je croyais qu’il faisait. Via CDP, contre un vrai Chrome headless : el.scrollTop = 100 émet 1 événement. Obscura avant correctif : 0. Après : 1. C’est la PR #458, mergée le 24 juillet, avec un test dédié de 5 cas dans crates/obscura-cdp/tests/scroll_event_on_assignment.rs.

Sans ce recoupement, un correctif peut avoir l’air de marcher tout en restant une divergence. La suite de tests interne te dit que ton code fait ce que tu as écrit ; seul Chrome te dit si tu as écrit la bonne chose.

Corriger en amont, pas dans son coin

Corriger en amont impose plus qu’un patch local : un test de régression, un périmètre lisible, et l’acceptation que quelqu’un d’autre tranche. Les 3 PR mergées suivent ce format.

  • #431, 19 juillet : Element.scrollBy, scrollTo et scroll n’existaient que sur window, et scrollTop ne faisait rien. Ajout dans bootstrap.js, vérifié par l’obstacle course 33/33.
  • #435, 22 juillet : l’abort en concurrence CDP de ma première issue. Chaque connexion CDP reçoit désormais son isolate V8 confiné, avec un verrou scopé par connexion. +983/−316 lignes, cargo nextest -p obscura-cdp 76/76, et environ 2,5× de débit à concurrence 8 par rapport au verrou global.
  • #458, 24 juillet : l’événement scroll à l’assignation, 91/91 tests après merge.

Deux autres PR, #442 et #460, portaient sur la géométrie de scroll : donner à une liste virtualisée un scrollHeight cohérent avec son contenu pour qu’elle pagine. L’approche à retenir se discute encore dans l’issue #441 ; en attendant, elle vit dans le fork.

Le fork qui suit l’amont

Pourquoi un fork si tout va en amont ? Parce qu’asap.cool tourne tous les jours et qu’une release amont n’arrive pas quand j’en ai besoin. Le FORK.md d’asap-cool/obscura dit le rôle : suivre l’amont en continu et figer un moteur que la plateforme maîtrise, pour la collecte concurrente de Google Maps. Le fork garde le nom de crate obscura pour que les merges depuis main s’appliquent proprement.

Sur 33 commits, 8 ne sont pas encore en amont : la branche scroll-geometry, celle des PR #442 et #460. Ce n’est pas un écart choisi, c’est un écart mesuré. Sur une recherche « plombier marseille », Obscura passe de 31 à 110 résultats uniques avec ce carry. Sur « restaurant lyon », de 36 à 108.

La concurrence CDP et le benchmark

Le « ~2,5× » de la PR #435 ne vient pas d’une impression. Le 20 juillet, j’ai poussé 2 commits sur obscura-benchmark, le dépôt de mesure du projet : une mesure de threads, RSS et VSZ en fonction du nombre de connexions CDP actives, et un réglage pour laisser le processus se poser avant l’échantillon au repos.

C’est ce qui m’a permis de documenter le gain dans la PR plutôt que d’affirmer « c’est plus rapide ». Si tu fais tourner plusieurs connexions CDP sur un moteur, c’est la mesure que je referais en premier.

Stack et choix techniques

Obscura est un workspace Cargo : obscura-dom, obscura-net, obscura-browser, obscura-cdp, obscura-js, obscura-mcp et obscura-cli. Le JavaScript s’exécute dans un V8 réel embarqué via deno_core, pas dans un DOM simulé, et le serveur CDP en fait un remplaçant direct de Chrome headless pour Puppeteer et Playwright. Le dépôt est public sur github.com/h4ckf0r0day/obscura.

Côté asap.cool, un adapter dédié, apps/worker/src/adapters/obscura_cdp.rs, pilote Obscura pour le module sourcing. Le reste de la plateforme ne connaît que le port défini dans modules/sourcing/src/ports.rs. Si demain le moteur change, c’est cet adapter qui change. Choisir un moteur Rust pour un worker Rust, c’est aussi pouvoir lire le code du moteur quand il se comporte autrement que prévu, et c’est ce qui a rendu ces PR possibles.

Résultat

Ce sont des chiffres de contribution, relevés le 20 septembre 2026 sur le dépôt amont et le fork. Obscura reste un projet tiers que j’utilise et auquel je contribue, pas un produit à moi.

IndicateurValeur
PR mergées en amont3
Lignes modifiées (PR mergées)+1 209 / −326
Issues ouvertes (toutes closes)6
Commits sur le fork33
Gain de débit (concurrence 8, PR #435)~2,5×
Tests (suite obscura-cdp après #458)91/91

Questions fréquentes

Pourquoi pas Playwright ou Puppeteer + Chromium pour des agents IA ?

Le problème n’est pas l’API, elle est très bien. C’est le poids d’un Chromium complet par session quand tu en fais tourner plusieurs en parallèle. Obscura et Lightpanda gardent CDP pour que ton code Playwright ou Puppeteer reste le même et remplacent le moteur en dessous. Tu changes l’adresse de connexion, pas ton script.

Comment vérifier qu’un comportement DOM corrigé se comporte comme Chrome ?

En comparant contre un Chrome headless réel via CDP, pas seulement contre la suite de tests du moteur. Sur la PR #458, la question tenait en une ligne : combien d’événements scroll pour el.scrollTop = 100 ? Chrome 1, Obscura 0 puis 1. Le test de 5 cas est ensuite arrivé pour que ça ne revienne pas.

Fork ou patch local, quand contribuer en amont ?

Chez moi, la correction va en amont dès qu’elle est mergeable, avec son test. Le fork existe pour une autre raison : figer un moteur que je maîtrise en production sans attendre chaque release. Ce qui reste dans le fork, la géométrie de scroll, y reste parce que l’approche se discute encore en amont, pas parce que j’ai choisi de diverger.

Combien de connexions CDP en parallèle avant que ça casse ?

Avant #435, deux navigations concurrentes suffisaient à faire aborter V8, parce qu’un timeout pouvait laisser un isolate V8 entré sur le thread, et que la connexion suivante déclenchait le contrôle interne de V8. Depuis, chaque connexion tourne sur son propre thread avec son isolate. Pour ta propre limite, obscura-benchmark mesure threads, RSS et VSZ par connexion active : c’est ce chiffre, sur ta machine, qui te dit où t’arrêter.


Si tu dépends d’un moteur ou d’une bibliothèque tierce en production et qu’un comportement difficile à reproduire te bloque, c’est le genre de diagnostic bas niveau que je prends en charge dans la durée en TMA et partenariat technique.

Un projet à concrétiser ?

Parlons-en. Je peux t'aider à transformer une idée en produit livré et itéré.

Un appel de 30 minutes, sans engagement. Je réponds sous 24 h. ★ Avis vérifiés sur Trustpilot