chromedp is een pure Go-bibliotheek, onder MIT-licentie, die een echte Chrome aanstuurt via het Chrome DevTools Protocol. De DOM wordt pas uitgelezen nadat de JavaScript van de pagina is uitgevoerd, rechtstreeks vanuit een Go-programma, zonder aparte WebDriver of Node-runtime. De Go-module compileert mee in de applicatie, maar voor het draaiende systeem is nog steeds een externe Chrome-executable nodig, waarvan de levensduur via Go-contexts wordt beheerd.
Installeren was simpelweg één go get plus een Chrome die ik zelf moest aanleveren: allocator opzetten, een context afleiden en Run een reeks acties geven. Op deze macOS arm64-host met een warme headless shell op schijf had een vers proces naar het eerste scriptresultaat een mediane tijd van 102 ms. Dat is een lokale basislijn, geen algemene claim dat opstarten nooit een bottleneck is. Op een fixture die een link 800 milliseconden na het laden invoegt, gaven twee van de vier leesstrategieën al antwoord voordat die link bestond.
De browser was echt, de content was er, en de code wachtte er gewoon niet op. Dat is misschien wel de nuttigste les die chromedp mij gaf, en het is geen bug — het is het verschil tussen ‘ik heb de pagina gerenderd’ en ‘ik heb gewacht op wat ik echt wilde hebben’. In de folklore rond headless browsers wordt dat verschil vaak weggepoetst. De wachstrategie, niet de browser, bepaalt of je data krijgt. Elk getal hier komt uit een lokale fixture die ik volledig onder controle heb, met de ground truth vooraf vastgelegd, en de ruwe samenvattingen staan in de chromedp-map van onze benchmarkrepo.
Wat chromedp eigenlijk is
De chromedp-stack is bewust compact gehouden. Geen Selenium-server. Geen WebDriver-shim. Geen Node-runtime onder de motorkap. Je Go-programma opent een WebSocket naar een Chrome-instance en praat daar direct CDP mee — in grote lijnen hetzelfde wire protocol dat Puppeteer gebruikt, maar dan zonder JavaScript ertussen.
De repo stond op 13.212 sterren en 178 open issues toen ik op 27 juli 2026 keek, onder een MIT-licentie. De versie die ik testte is v0.16.0, en dat is de nieuwste tag in de repo. Belangrijk om te noemen, zodat het niet verwarrend wordt: GitHub’s Releases-pagina laat nog steeds v0.15.1 (gepubliceerd op 2026-04-01) zien als nieuwste release-object, terwijl go get github.com/chromedp/chromedp@latest uitkomt op v0.16.0. Go-modules en GitHub-release-objecten zijn hier uit elkaar gelopen. Niet kapot, wel irritant als je probeert uit te zoeken wat je precies draait.
Het denkmodel is helemaal gebaseerd op Go-contexts. Je maakt een allocator-context (die weet hoe Chrome te starten), leidt daar een browser-context uit af, en roept vervolgens chromedp.Run(ctx, actions...) aan met een lijst Actions. Een child-context van een browser-context is een nieuw tabblad. Annuleer je een context, dan verdwijnt het ding dat die context representeert. Als je al eens met Go-concurrency hebt gewerkt, voelt dit meteen vertrouwd aan; en zo niet, dan is onze handleiding om te starten met webscraping in Go een mildere instap dan de godoc van chromedp.
Eén grens moet je vroeg zetten: chromedp levert je een gerenderde DOM. Het levert je geen gestructureerde data. Alles wat je uit die DOM haalt — velden, tabellen, prijzen — is code die je zelf schrijft en onderhoudt. Het is een driver, geen scraperframework.
De machine onder de motorkap
Alles in chromedp is een Action, en Run voert een slice daarvan in volgorde uit op een target. Navigate, Click, Evaluate, OuterHTML, WaitVisible — allemaal dezelfde interface, allemaal te combineren, allemaal gewoon CDP-commando’s in Go-verpakking. Die uniformiteit is de beste ontwerpkeuze van de bibliotheek, omdat het betekent dat de gebruiksvriendelijke laag en het ruwe protocol op hetzelfde niveau leven.
Dat is belangrijk, omdat die gebruiksvriendelijke laag bewust dun is gehouden. chromedp is gebouwd op cdproto, een gegenereerde set getypeerde Go-bindings die het volledige DevTools Protocol afdekken, en de chromedp-godoc documenteert beide lagen naast elkaar. Bestaat de convenience-action niet, dan zak je gewoon af naar de domain call — network.Enable(), page.CaptureScreenshot(), runtime.Evaluate() — binnen dezelfde Run. Er zit geen muur tussen ‘de fijne API’ en ‘de echte API’, en dat geldt niet voor elke browserdriver.
De wait-acties zijn waar de dagelijkse keuzes gemaakt worden, en er zijn er meer dan de meeste mensen gebruiken:
| Wait-actie | Waarop wordt gewacht |
|---|---|
WaitReady(sel) | totdat de node aan de DOM gekoppeld is |
WaitVisible(sel) | totdat de node daadwerkelijk zichtbaar is |
WaitNotPresent(sel) / WaitNotVisible(sel) | het omgekeerde, handig voor loaders |
Poll(js, res) | een JavaScript-predicaat periodiek evalueren totdat het waar is |
Procesbeheer is het andere stuk machine dat je moet kennen, omdat het bepaalt of je programma een browser achterlaat. chromedp start Chrome via Go’s exec.CommandContext. Het annuleren van die context doodt het proces. Dat ene implementatiedetail verklaart zowel het goede gedrag als de scherpe rand waar ik in de test tegenaan liep.
De setup is een Go-binary plus een Chrome die je zelf moet aanleveren
go get github.com/chromedp/chromedp liep zonder gedoe netjes binnen op v0.16.0, en de dependency tree bevat geen cgo-imports. Dus de regel ‘pure Go, zonder externe dependencies’ die je vaak terugziet, klopt — voor de Go-module.
Maar niet voor de runtime. chromedp stuurt een externe Chrome aan, en zonder Chrome op het systeem mislukt een run direct. Voor elke meting leverde ik het exacte executable-pad aan via chromedp.ExecPath, gericht op een Chrome for Testing-headless shell 151.0.7922.10. Dat is geen kritiek — een browser aansturen vereist nu eenmaal een browser — maar ‘geen externe dependencies’ en ‘je moet een 155 MB Chrome naast je binary shippen’ zijn twee heel verschillende deployverhalen, en maar één daarvan staat expliciet in de README.
Er zat nog een tweede valkuil in de setup die me tijd kostte en die je beter vooraf weet. chromedp-issue #1591 meldt dat de go test-runner in Go 1.25+ NewExecAllocator al tijdens het opstarten annuleert; exact dezelfde code werkt prima als gecompileerde binary. Ik heb daarom een probe-binary met go build gemaakt en die voor elke meting gebruikt, in plaats van iets via go test te draaien. Go was hier 1.26.5, macOS arm64. Als je eerste chromedp-ervaring een testfile is die tijdens het starten van Chrome sterft, lees dan eerst dat issue voordat je je eigen code de schuld geeft.
Praktisch: vier manieren om dezelfde pagina te lezen, waarvan er twee leeg terugkomen

De fixture is een lokale server op 127.0.0.1 die drie soorten content serveert die alleen verschillen in wanneer ze in de DOM verschijnen: een statische <a> in de meegeleverde bytes, een <a> die door een inline <script> tijdens de eerste parse wordt aangemaakt, en een <a> die door setTimeout een configureerbaar aantal milliseconden na het load-event wordt toegevoegd. De markers en hrefs van de twee script-gegenereerde links worden in JavaScript uit stringfragmenten opgebouwd, zodat er nergens in de serverbytes een aaneengesloten letterlijke string bestaat. Een ‘gevonden’ resultaat bewijst dus dat Chrome JavaScript heeft uitgevoerd, niet dat iemand HTML heeft uitgelezen.
Recall wordt berekend in Python tegen vooraf geregistreerde ground-truth markers, niet in de Go-probe zelf, dus de probe kan niet vals spelen door het antwoord al te kennen. Elke strategie liep drie keer; de gevonden sets waren alle drie de keren identiek.
| Leesstrategie | Statische HTML-link | Ingevoerd tijdens parse | Ingevoegd 800 ms na load | Verstreken tijd |
|---|---|---|---|---|
Navigate + lezen, zonder wachten | gevonden | gevonden | gemist | 317 ms |
WaitReady("body") | gevonden | gevonden | gemist | 107 ms |
WaitVisible("#delayed-injected") | gevonden | gevonden | gevonden | 912 ms |
| Poll totdat de marker verschijnt | gevonden | gevonden | gevonden | 972 ms |
Twee regels komen terug met twee van de drie links. De naïeve read mist de derde link omdat Navigate terugkeert op het load-event en die derde link dan nog niet bestaat. WaitReady("body") mist om een subtielere reden die in de praktijk nog vervelender is: body is bij load al gekoppeld, dus de wait is meteen klaar en het voelt alsof je de juiste, verantwoordelijke stap hebt gezet. Hij kwam terug in 107 ms, sneller dan de no-wait-route, en gaf je dezelfde incomplete pagina.
Om het mechanisme te bevestigen in plaats van het te veronderstellen, heb ik de injectievertraging gesweept en beide uitersten opnieuw gedraaid (recall-summary.json):
| Injectievertraging na load | No-wait leest het | WaitVisible ziet het | WaitVisible verstreken tijd |
|---|---|---|---|
| 0 ms | ja (race) | ja | 109 ms |
| 100 ms | nee | ja | 208 ms |
| 400 ms | nee | ja | 519 ms |
| 800 ms | nee | ja | 911 ms |
| 1500 ms | nee | ja | 1625 ms |
WaitVisible’s verstreken tijd volgt op deze fixture de injectievertraging — 100 naar 208, 400 naar 519, 800 naar 911, 1500 naar 1625 — bewijs dat het wachtte tot de node verscheen in plaats van te vroeg te lezen. De rij met 0 ms is de grens: setTimeout(…, 0) kan vuren vóór de directe read, dus de no-wait-route kan het soms nog meenemen. Vanaf 100 ms en hoger miste de no-wait-route het in elke run.
In productie kan dezelfde timingfout leiden tot geldige HTML met nul geëxtraheerde rijen en toch exitcode 0, tenzij de pipeline de output-cardinaliteit controleert. Dat is een plausibele faalmodus, ondersteund door het gedrag van de fixture, maar hier niet als incident gemeten. Renderen is maar de helft van de eis; de read moet wachten op een applicatieniveau-conditie die aan de gewenste data gekoppeld is.
WaitReady en WaitVisible zijn niet ‘gerankt’ — ze beantwoorden verschillende vragen
De gebruikelijke formulering is dat WaitVisible ‘betrouwbaarder’ is dan WaitReady. Dat is onnauwkeurig genoeg om schadelijk te zijn. Op een pagina met een node die wel aan de DOM hangt maar met display: none gestyled is, lopen de twee duidelijk uiteen (waitsem-summary.json, drie identieke runs):
| Doelnode | Actie | Resultaat | Tijd |
|---|---|---|---|
gekoppeld, display:none | WaitReady | keert terug | ~6 ms |
gekoppeld, display:none | WaitVisible | time-out, context deadline exceeded | 4000 ms |
| zichtbare node | WaitVisible, standaard query | keert terug | 4–12 ms |
| zichtbare node | WaitVisible, ByID | keert terug | 1–2 ms |
| zichtbare node | WaitVisible, ByQuery | keert terug | 1 ms |
WaitReady betekent gekoppeld. WaitVisible betekent zichtbaar. Stel je de verkeerde vraag, dan glijd je óf voorbij content die nooit gerenderd is, óf je blijft hangen tot de volledige timeout op een node die sowieso nooit zichtbaar ging worden. Het deadline-gedrag zelf is netjes — een keurige context deadline exceeded precies op 4 s, zonder hang, zonder zombie-toestand — en dat is al meer dan sommige drivers weten te leveren.
Eén gemelde valkuil bleek niet reproduceerbaar. Issue #440 meldt dat WaitVisible("#id") met de standaard query blijft hangen, en dat kon ik op v0.16.0 niet reproduceren — standaard query, ByID en ByQuery keerden allemaal terug op de zichtbare node, in elke run. Niet reproduceerbaar is niet hetzelfde als opgelost: dit is één selectorvorm op één pagina, en daarmee is het issue niet afgedaan.
De defer cancel() die je overslaat, houdt het dak overeind
Niet de return values, maar de procesaantallen tellen. Elke lifecycle-run gebruikte een unieke --user-data-dir en telde daadwerkelijke Chrome-browserprocessen met pgrep, waarbij renderer-childprocessen werden gefilterd. Elk pad liep drie keer (lifecycle-summary.json).
| Exit-pad (macOS, 3 runs elk) | Wat er gebeurde met de opgestarte chrome-headless-shell | Timing |
|---|---|---|
| Context en allocator annuleren | verdwenen | 13, 13 en 12 milliseconden |
| Het Go-proces beëindigen zonder annuleren | overleeft je programma — nul browserprocessen vóór, één na het verlaten van de probe; wees in drie van drie runs verweesd | — |
Annuleren is schoon, snel en precies wat exec.CommandContext belooft. (Elke verweesde browser is daarna door de harness geforceerd beëindigd; de host is weer netjes opgeschoond.)
Dit gedrag is bekend, gedocumenteerd en platformgebonden — de meting is van mij, de ontdekking niet. De tracker van chromedp behandelt dit vanuit meerdere invalshoeken: #774 beschrijft hetzelfde niet-afsluiten op FreeBSD, #752 meldt hangende Chromium-processen op macOS, en #562 plus #1566 leggen het mechanisme uit. Wat ik heb toegevoegd, zijn de procestelling en de timing aan beide kanten van het contrast; die kwalitatieve meldingen geven dat niet.
Het mechanisme zelf hangt samen met build tags. In de bron van v0.16.0 zet allocate_linux.go Pdeathsig = SIGKILL op het child-proces, dus Linux krijgt een kernel-level parent-death-signal. allocate_other.go, wat macOS compileert, maakt die call een no-op. Op darwin bestaat geen equivalente signal, dus Chrome wordt niet gedood wanneer je programma afsluit. Ondertussen leest de godoc alsof er een algemene belofte wordt gedaan — het standaardcommando ‘stuurt SIGKILL naar alle open browsers wanneer het Go-programma eindigt’ — terwijl de Linux-afbakening alleen in build-tagged broncode staat die je zelf moet opzoeken. Dat de documentatie iets te ruim klinkt, is fair; dat chromedp zelf kapot is, niet.
De praktische consequentie blijft in beide gevallen hetzelfde: op macOS is defer cancel() essentieel. Sla je die over, dan lekt elke run een browserproces. Ik heb Linux niet getest, dus ik generaliseer dat orphan-resultaat daar niet naar; de bron suggereert dat Linux zich anders gedraagt, en suggereren is nog geen meting.
Cold start, concurrency en de saaie details die je deployment bepalen

102 ms was de mediane tijd van een vers proces, allocator, context, localhost-navigatie en eerste Evaluate, over vijf processen, met een spreiding van 98 tot 111 ms (coldstart-summary.json). Op deze macOS arm64-fixture met een warme headless shell op disk was startup klein vergeleken met het wachten op vertraagde content. Containers, koude filesystems, CI, serverless-omgevingen en productie-navigatie zijn niet gemeten.
Voor concurrency geeft chromedp je twee vormen: één browser met meerdere child-contexts (tabbladen), of meerdere onafhankelijke browsers. Vier navigaties, drie runs per vorm (concurrency-summary.json):
| Mode | Totale tijd (p50) | Spreiding | Piek aantal Chrome-browserprocessen |
|---|---|---|---|
| Gedeelde browser, 4 child-contexts | 214 ms | 209–219 | 1 |
| 4 aparte browsers | 264 ms | 261–278 | 4 |
Het gemeten verschil is het aantal processen: één Chrome-browserproces versus vier voor deze vier triviale lokale navigaties. De tijdsreeksen overlappen niet, maar blijven richtinggevend en vormen geen throughput-benchmark. RSS en PSS zijn niet gemeten, dus deze test bewijst geen geheugenbesparing.
De kleine error-path-probe is hier niet gedetailleerd genoeg om een robuustheidsclaim te dragen: het concept noemt niet of HTTP-status, navigatiefout, event of harnesslogica elke conditie zichtbaar maakte. Behandel 500/dead-link-afhandeling daarom als ongerapporteerd totdat de exacte API-uitkomst en het ruwe artifact gepubliceerd zijn.
Niet getest, en dus buiten wat deze cijfers afdekken: Linux lifecycle-gedrag, concurrency verder dan N=4 of met echt werk per pagina, geheugendelta’s (ik telde processen, geen RSS), netwerkinterceptie en request capture, en de open WaitReady-timeoutmeldingen in #168 en #1593 — die beschrijven intermitterende time-outs, terwijl wat ik mat de semantiek van waits is, een andere vraag. Eén machine, één Chrome-build.
chromedp is niet de enige Go CDP-driver op deze bench: rod is door exact dezelfde fixture, harness, host en Chrome-build gelopen, in dezelfde sessie, en krijgt een eigen bespreking.
Voor- en nadelen
Voordelen:
- Echte CDP-toegang — de convenience-actions en de ruwe
cdproto-domain calls zijn samen te gebruiken binnen dezelfdeRun, dus je loopt nooit tegen een APIplafond aan. - Lokale cold-cycle-baseline van 102 ms p50 met een spreiding van 98–111 ms op de geteste macOS-fixture.
- Annuleren ruimt Chrome consequent op in ongeveer 13 ms, bij elke run.
- Child-contexts delen één browserproces voor N tabbladen (1 proces versus 4 bij aparte browsers).
- Deterministisch in tests — recall-sets, wait-semantiek en lifecycle-uitkomsten waren drie keer achter elkaar identiek.
- Schone deadline-afhandeling:
WaitVisibleop een onbereikbare conditie gaf exact na 4 s een nettecontext deadline exceededin plaats van te hangen. - Pure-Go-module (geen cgo), MIT-licentie; runtime vereist nog steeds een externe Chrome-executable.
Nadelen:
- Vereist runtime een externe Chrome; de reputatie van ‘geen dependencies’ gaat alleen over de Go-module.
WaitReady("body")is een valkuil die goed voelt, maar post-load content stilletjes mist — hij keerde terug in 107 ms met een onvolledige pagina.- De naïeve
Navigate+ read-route mist alles wat ≥ ~100 ms na load wordt ingevoegd, deterministisch en zonder foutmelding. - Op macOS laat afsluiten zonder
cancel()de browser verweesd achter (3/3 runs). Bekend en platformgebonden, maar makkelijk om over te struikelen. - De SIGKILL-bij-exit-beschrijving in de godoc klinkt universeel, terwijl het mechanisme alleen in Linux build-tagged broncode zit.
go testop Go 1.25+ kan allocator-startup annuleren (#1591); bouw liever een binary.- Het levert een DOM, geen gestructureerde data — elk veld dat je wilt, vereist eigen parsingcode die je moet onderhouden.
- De nieuwste tag (v0.16.0) ligt voor op het nieuwste GitHub Release-object (v0.15.1), wat versiecontrole even verwarrend maakt.
Voor wie chromedp bedoeld is, en wie het beter kan overslaan
Als je service al in Go is geschreven en je wilt daar een echte browser in hebben, dan is chromedp bijna de voor de hand liggende keuze. Geen Node-proces om te beheren, geen WebDriver-server om in leven te houden, één gecompileerde binary plus een Chrome die je meelevert of installeert. Het context-model sluit zo direct aan op Go’s concurrency-primitieven dat browserlevenscycli uiteindelijk worden beheerd met dezelfde defer-discipline als de rest van je codebase. En als je iets nodig hebt dat de convenience-API niet afdekt — CDP-networkevents, precieze page-lifecycle-hooks, protocol-level trucs — stap je zonder de bibliotheek te verlaten over naar cdproto.
Het is ook een goede keuze als je expliciete controle over wachten wilt. De wait-acties zijn primitives, geen heuristieken; ze doen exact wat ze zeggen. Dat is een voordeel zodra je accepteert dat het kiezen van de juiste stap nu jouw verantwoordelijkheid is.
Sla het over als je team geen Go schrijft — de taalkeuze is de echte kostenpost, niet de bibliotheek. Sla het over als je de ergonomie van auto-waiting wilt die het voor je raadt, want chromedp gaat niet raden; het doet precies wat je vraagt en geeft terug wat de pagina op dat moment had. Sla het over, of budgetteer er serieus voor, als je eigenlijk gestructureerde records nodig hebt in plaats van een DOM: elk veld is een selector die je zelf schrijft, test en repareert wanneer de site verandert. En als ‘geef me gewoon de data van deze 500 URL’s’ de hele eis is, dan is browserorkestratie opzetten in Go nogal wat machinerie voor die vraag. Onze gids voor browserautomatisering laat zien wanneer die machinerie zichzelf terugverdient en wanneer niet.
Alternatieven, inclusief waar onze eigen stack past
Binnen de browserdriver-categorie is rod ook een Go CDP-driver, terwijl Playwright en Puppeteer Node-opties zijn die we bespreken in onze uitleg Playwright versus Puppeteer. Hun waiting-contracten zijn niet uitwisselbaar. Playwright wacht vaak automatisch tot een actie uitvoerbaar is; dat zegt nog steeds niet wanneer applicatiedata na de actie volledig is aangekomen. chromedp geeft je laagdrempelige wait-primitives en laat zowel actionability als applicatieniveau-gereedheid aan de aanroeper over. Als je het bredere veld verkent, behandelt onze overzichtspagina van open-source scrapers die we hebben getest statische crawlers en extractiebibliotheken aan de andere kant van deze lijn.
Gerelateerde review: Browserless review.
Een beheerde extractieservice is een andere categorie. Die ruilt browser- en selectorcontrole in voor uitbestede rendering en schema-opbouw. Dat kan handig zijn wanneer het eindresultaat gestructureerde records zijn in plaats van een DOM, terwijl chromedp beter past wanneer de browser onder controle van je Go-service moet blijven. Wij bouwen Thunderbit, zo’n dienst, maar hebben die niet tegen deze fixture gedraaid; deze test ondersteunt daarom geen vergelijking op gelijkwaardigheid, latency, extractiekwaliteit of kosten met chromedp.
Probeer Thunderbit voor webdata-extractie
Conclusie
Moet je chromedp gebruiken? Ja, als je Go schrijft en een echte browser onder eigen controle wilt. In deze lokale fixture bereikte het het eerste scriptresultaat in 102 ms p50, ruimde het Chrome op in ongeveer 13 ms na annuleren, en gebruikte het één browserproces voor vier gelijktijdige tabbladen. Dat zijn afgebakende observaties, geen universele prestatiebeloften; de blijvende aantrekkingskracht is directe CDP-toegang vanuit Go wanneer de convenience-laag niet meer volstaat.
Wel moet je de claims goed afbakenen, want de reputatie verkoopt twee dingen te ruim. ‘Pure Go, geen dependencies’ beschrijft de module; runtime moet je nog steeds een Chrome-binary leveren en beheren. En ‘gebruik een headless browser en je krijgt de dynamische content’ klopt alleen wanneer je wait gekoppeld is aan de node die je wilt — een naïeve read en WaitReady("body") gaven mij allebei een pagina terug waarin content ontbrak die 800 ms na load werd ingevoegd, stilletjes, elke keer weer. Op macOS is defer cancel() geen stijlvoorkeur; sla je het over, dan lekt elke run een browser, en dat is bekend platformgedrag maar nog steeds jouw probleem om op te lossen. Als je die drie dingen goed doet, is chromedp een van de voorspelbaardere browserdrivers die ik heb gemeten. Doe je ze fout, dan faalt het stilletjes — en dat is de slechtste manier waarop een scraper kan falen.
Probeer Thunderbit voor webdata-extractie Get Started Free
FAQs
Ziet chromedp echt content die door JavaScript is gerenderd?
Ja — maar alleen met een wait die aan de node is gekoppeld die je wilt hebben. Op een fixture waar een link 800 ms na het load-event werd ingevoegd, miste Navigate plus lezen die link en miste WaitReady("body") die ook, terwijl WaitVisible op die node en een JavaScript-poll hem allebei wel terugvonden, drie van de drie runs. Als je de vertraging opschuift, mist de no-wait-read de node bij elke instelling vanaf 100 ms. Renderen is noodzakelijk; correct wachten maakt het voldoende.
Wat is het verschil tussen WaitReady en WaitVisible?
WaitReady wacht tot de node aan de DOM gekoppeld is. WaitVisible wacht tot hij echt zichtbaar is. Op een node die wel gekoppeld is maar display: none heeft, kwam WaitReady na ongeveer 6 ms terug, terwijl WaitVisible volledig doorliep tot de context-deadline van 4 seconden en netjes context deadline exceeded gaf. Geen van beide is ‘betrouwbaarder’ — ze beantwoorden gewoon verschillende vragen, en de verkeerde kiezen is de echte valkuil.
Heb ik echt defer cancel() nodig met chromedp?
Op macOS: ja. Het annuleren van de context en allocator ruimde de opgestarte Chrome in elke run op binnen 12–13 ms; het Go-proces afsluiten zonder te annuleren liet in alle drie runs een verweesd browserproces achter. Dit is bekend, platformgebonden gedrag — de tracker van chromedp documenteert hetzelfde niet-afsluitpatroon op andere niet-Linux systemen, en de parent-death-kill die het oplost zit in Linux-only build-tagged broncode. Ik heb Linux niet getest, dus beschouw het orphan-resultaat als macOS-gebonden.
Moet chromedp Chrome apart geïnstalleerd hebben?
Ja. De Go-module zelf is pure Go zonder cgo, maar stuurt een externe browser aan en faalt direct als die ontbreekt. Ik leverde expliciet een Chrome for Testing 151.0.7922.10 headless shell aan via chromedp.ExecPath. Het voordeel is dat de opstartkosten klein zijn: een volledige cold cycle tot het eerste scriptresultaat had een mediane tijd van 102 ms over vijf verse processen, met een spreiding van 98–111 ms.
Moet ik één browser delen over tabbladen of aparte browsers starten? Kies child-contexts als het minimaliseren van browserprocessen de prioriteit is. Vier localhost-navigaties via één browser gebruikten 1 Chrome-browserproces; vier aparte browsers gebruikten 4. Ook de totale tijd viel uit in het voordeel van de gedeelde opzet (214 ms versus 264 ms mediaan), maar vier triviale lokale pagina’s zijn geen throughput-benchmark. Geheugen is niet gemeten. Aparte browsers kunnen nog steeds de juiste keuze zijn als je sterkere sessie-isolatie, verschillende proxies of een kleinere crash-impact wilt; die afwegingen vielen buiten deze test.


