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.
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,scrollToetscrolln’existaient que surwindow, etscrollTopne faisait rien. Ajout dansbootstrap.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-cdp76/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.
| Indicateur | Valeur |
|---|---|
| PR mergées en amont | 3 |
| Lignes modifiées (PR mergées) | +1 209 / −326 |
| Issues ouvertes (toutes closes) | 6 |
| Commits sur le fork | 33 |
| 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