私たちの利害: 私たちは Geonode でプロキシを売っており、ブラウザ自動化はプロキシ経由でできることの中で最も帯域を食います。ヘッドレスブラウザは画像、フォント、スクリプト、動画プリロードをすべて取得するので、従量トラフィックで chromedp を動かすコストは、同じページの生 HTTP リクエストよりおよそ一桁高いです。 削り方の節があり、そこでの手法は安いプロバイダを選ぶより多く節約します。プロキシ設定自体は3行で、本当の落とし穴が1つあり、それも扱います。
chromedp とは
プロジェクトは自らを “a faster, simpler way to drive browsers supporting the Chrome DevTools Protocol in Go without external dependencies” と述べます。
最後の句が売りです。Selenium はブラウザ版に合わせた driver バイナリが必要で、Playwright は独自ランタイムを同梱します。chromedp は websocket 上で DevTools Protocol を直接話すので、Go バイナリと Chrome のインストールがデプロイの全部です。
MIT ライセンスで活発に保守されています。0.15.1 は 2026年4月リリース、コミットは7月まで続き、2026年9月に確認しました。
インストールは平凡です。
go get -u github.com/chromedp/chromedp
生成されたプロトコルバインディングは同伴パッケージ github.com/chromedp/cdproto にあり、高レベル API が包まないものが必要なときに使います。
Context モデル
最初に理解すべき部分です。残りはここから来ます。
chromedp は context.Context を同時に2つの仕事に使います。Go が常にするキャンセルと、ブラウザとタブのハンドルを運ぶことです。この二重の用途が、セットアップがあのような形に見える理由です。
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var title string
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.Text("h1", &title, chromedp.NodeVisible),
)
最初の NewContext がブラウザを割り当てます。 そこから派生した後続の contexts は同じブラウザに新しいタブを作り、起動コストを繰り返し払わずに複数ページを動かせます。
browserCtx, cancelBrowser := chromedp.NewContext(context.Background())
defer cancelBrowser()
tabCtx, cancelTab := chromedp.NewContext(browserCtx)
defer cancelTab()
キャンセルは閉じます。 タブ context をキャンセルするとタブが閉じ、ブラウザ context をキャンセルするとブラウザが閉じます。defer cancel() は任意の帳簿ではありません。省略すると Chrome プロセスが漏れます。
タイムアウトは普通の Go の合成です。
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
プロジェクト自身の FAQ にある2つのエラーは先に知っておく価値があります。
“Executing an action without Run results in 'invalid context'.” FAQ は “by default, a chromedp context does not have an executor, however one can be specified manually if necessary” と説明します。Actions は自分では実行されません。Run が実行する値です。
“I'm seeing 'context canceled' errors.” FAQ は接続喪失に帰します。“when the connection to the browser is lost, chromedp cancels the context, and it may result in this error. This occurs, for example, if the browser is closed manually, or if the browser process has been killed or otherwise terminated.” したがって context canceled はタイムアウトではなく Chrome が死んだことをしばしば意味します。タイムアウトを上げる前に区別する価値があります。
Actions
Run は actions の列を受け取り、順に実行します。よく使うものは大半の仕事をカバーします。
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com/search"),
chromedp.WaitVisible(`input[name="q"]`),
chromedp.SendKeys(`input[name="q"]`, "golang"),
chromedp.Click(`button[type="submit"]`, chromedp.NodeVisible),
chromedp.WaitVisible(`.results`),
chromedp.Text(`.results`, &results, chromedp.NodeVisible),
)
複数要素からの抽出は Nodes か Evaluate を使います。
var links []string
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.Evaluate(`[...document.querySelectorAll('a')].map(a => a.href)`, &links),
)
Evaluate はページ内で JavaScript を実行し、結果を Go 値にアンマーシャルします。複数要素を一度に扱うならしばしば最短経路です。値は JSON シリアライズ可能でなければなりません。
複数値を返す actions には、FAQ がラッパーを示します。
chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
_, err := domain.SomeAction().Do(ctx)
return err
}))
ActionFunc は、高レベル API がカバーしないこと — cookie 設定、ネットワークリクエストの傍受、デバイスエミュレーション — のために生の cdproto 呼び出しへ降りる方法でもあります。この非常口は DevTools Protocol 全体に使え、それは大きな面です。
正しく待つ
信頼できるスクレイパーと不安定なものの差で、間違いはいつも同じです。
sleep しない。 chromedp.Sleep(3*time.Second) は存在し、魅力的で、短すぎて遅い日に間欠失敗するか、長すぎて毎回時間を無駄にします。普通は両方で、マシン次第です。
気になるものを待つ:
chromedp.WaitVisible(`.results`, chromedp.ByQuery)
chromedp.WaitNotVisible(`.spinner`)
chromedp.WaitReady(`#content`)
WaitVisible は要素が存在しかつ見えるまで待ち、WaitReady は DOM に存在するまで待ちます。操作後に載る内容では、見えることが通常正しい条件です。
セレクタが表せない条件は、ページ内で poll します。
chromedp.Poll(`document.querySelectorAll('.item').length >= 20`, nil)
これが「リストの読み込みが終わるまで待つ」への答えで、要素ベースの待ちでは表せません。
待ちは必ず context タイムアウトで上限を付ける。 決してマッチしないセレクタへの WaitVisible は context が切れるまでブロックし、タイムアウトがなければ永遠です。
ヘッドレス実行、そして Docker
Chrome はデフォルトでヘッドレスです。FAQ は最初の疑問に答えます。“By default, Chrome is run in headless mode. See DefaultExecAllocatorOptions, and an example to override the default options.”
開発中に動きを見るには:
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false),
)
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancelAlloc()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
NewExecAllocator が Chrome のコマンドラインフラグを置く場所で、ブラウザ context の上の層です。
コンテナ向けに、プロジェクトの推奨は具体的です。“The simplest way is to run the Go program that uses chromedp inside the chromedp/headless-shell image. That image contains headless-shell, a smaller headless build of Chrome, which chromedp is able to find out of the box.”
従う価値があります。コンテナで動く Chrome を手で組み立てることは、欠けた共有ライブラリとフォントパッケージを追うことで、結果は専用イメージより大きくなります。
FAQ の Linux 固有の挙動は、Chrome を別に動かす人を驚かせます。“On Linux, chromedp is configured to avoid leaking resources by force-killing any started Chrome child processes. If you need to launch a long-running Chrome instance, manually start Chrome and connect using RemoteAllocator.”
RemoteAllocator は既に動いているブラウザの websocket エンドポイントに接続します。共有ブラウザプールや別コンテナのブラウザのパターンです。
プロキシを使う
3行と、1つの落とし穴。
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.ProxyServer("http://proxy.example.com:9000"),
)
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancelAlloc()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
ProxyServer は Chrome の --proxy-server フラグを設定します。
落とし穴は認証です。 Chrome の --proxy-server は資格情報を取りません。ユーザー名とパスワード付き URL では認証されません。Chrome はプロキシチャレンジにダイアログで応え、ヘッドレスブラウザには入力する人がいません。
回避は2つ、好みの順です。
IP 許可リストを使う。 送信元アドレスが安定ならプロバイダに登録し、資格情報を捨てます。最もきれいな答えで、迂回ではなく問題を消します。
認証イベントを扱う。 chromedp は handleAuthRequests 付きの fetch.Enable で DevTools Protocol の認証要求に応え、プログラムで資格情報を渡せます。コードは増え、アドレスが固定でないときの経路です。
それから本当に効いたか確認する。 ブラウザ内の誤設定プロキシは静かです。
var ip string
err := chromedp.Run(ctx,
chromedp.Navigate("https://api.ipify.org"),
chromedp.Text("body", &ip, chromedp.NodeVisible),
)
プロキシありなしで実行してください。アドレスが変わらなければ Chrome は使っておらず、何も教えてくれません。地域向け作業ではさらに進み、地域で異なる内容が本当に違うことを確認してください。アドレスは簡単な半分です。これが なぜプロキシのテストが重要か で述べたサイレント失敗パターンです。
ついでに locale を出口国に合わせる。 ドイツ出口に en-US 言語ヘッダとロンドンのタイムゾーンは、実在の訪問者が出さない組み合わせで、多くのサイトはアドレスとは独立に locale を使います。
chromedp.Flag("lang", "de-DE"),
帯域を削る
いちばんお金を節約する節で、あらゆるブラウザ自動化に当てはまります。
200 KB の HTML ページは、画像、フォント、追跡スクリプト、動画プリロードをすべて取ると 4 MB になり得ます。住宅向け 0.79ドル/GB — 2026年9月の 料金ページ で確認した私たちの数字 — では、この差が予算の全部です。
不要なリソース種別をブロックする。 fetch.Enable と request-paused リスナーで、画像・メディア・フォントのリクエストを中止します。
chromedp.ListenTarget(ctx, func(ev interface{}) {
if e, ok := ev.(*fetch.EventRequestPaused); ok {
go func() {
c := chromedp.FromContext(ctx)
execCtx := cdp.WithExecutor(ctx, c.Target)
switch e.ResourceType {
case network.ResourceTypeImage, network.ResourceTypeMedia, network.ResourceTypeFont:
_ = fetch.FailRequest(e.RequestID, network.ErrorReasonBlockedByClient).Do(execCtx)
default:
_ = fetch.ContinueRequest(e.RequestID).Do(execCtx)
}
}()
}
})
これでトラフィックの大部分が日常的に削れ、副次的に実行も速くなります。
ブラウザを再利用し、タブを作る。 ブラウザ起動は高く、タブは安い。多ページの実行では一度割り当て、タブ contexts を派生させます。
そしてブラウザが本当に要るか問う。 内容が初期 HTML にあるなら、素の HTTP リクエストはごく一部のコストでずっと速い。自動化に手を出す前にページソースを見てください。すべてを描画する反射は、この領域で不要コストの最もありふれた源です。
何も動かないときのデバッグ
ブラウザ自動化の失敗は不透明です。決してマッチしないセレクタと決して載らないページは同じタイムアウトを出します。決まった手順で大半が解けます。
ブラウザを点けて見る。 いちばん速い診断で、人は最後に回します。
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false),
)
半分の場合、答えはすぐ見えます。ボタンを覆うクッキーバナー、ログインへのリダイレクト、チャレンジ画面、テストしたレイアウトとの違いです。
実行が失敗したときにページを捉える。 ヘッドレス環境では見る代わりになります。
var buf []byte
_ = chromedp.Run(ctx, chromedp.FullScreenshot(&buf, 90))
_ = os.WriteFile("failure.png", buf, 0644)
HTML と組にしてください。スクリーンショットは描画されたものを、ソースは届いたものを示します。
var html string
_ = chromedp.Run(ctx, chromedp.OuterHTML("html", &html, chromedp.ByQuery))
タイミング問題と決める前に、セレクタが解決するか確認する。
var count int
_ = chromedp.Run(ctx, chromedp.Evaluate(`document.querySelectorAll('.item').length`, &count))
ゼロはセレクタ問題で、どれだけ待っても直りません。
ページ自体がエラーしている疑いがあるときはブラウザログを有効にする。
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("enable-logging", true),
chromedp.Flag("v", "1"),
)
コンソールメッセージと失敗リクエストを聞く。 空に描画されるページをしばしば説明します。
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch e := ev.(type) {
case *runtime.EventConsoleAPICalled:
log.Printf("console.%s", e.Type)
case *network.EventLoadingFailed:
log.Printf("failed: %s %s", e.Type, e.ErrorText)
}
})
API 呼び出しがすべて失敗しているページは、セレクタが変わったページと見た目が同じで、ネットワークイベントだけが区別します。
最後の手段として chromedp-proxy を使う。 プログラムとブラウザの間に入り、DevTools Protocol トラフィックを双方向に記録します。挙動が全く意味をなさないとき、実際のプロトコル交換を見ればたいてい一度で説明がつきます。
chromedp が正しい選択になるとき
使うのは すでに Go を書いていて、配布する driver なしの単一静的バイナリが欲しいとき、または上位ツールが公開しない直接の DevTools Protocol アクセスが必要なときです。
Playwright を検討するのは クロスブラウザ、各 action に組み込まれた自動待ち、失敗時のトレースとスクリーンショット、より大きなドキュメントと例が欲しいときです。Go 移植はありますが、生態系は JavaScript と Python が中心です。
素の HTTP を検討するのは 内容が初期応答にあるときです。速く、安く、単純で、議論が示唆するより多くのウェブが有用な HTML を出します。
FAQ 自身のリソース一覧は次の行き先の良い地図です。複雑な actions と全ページスクリーンショットの examples リポジトリ、生成プロトコル API の cdproto リファレンス、プログラムとブラウザが互いに何を言っているかを見る CDP ログプロキシ chromedp-proxy — 最後の手段のデバッグツールであり、本当に良いものです。
よくある質問
chromedp とは?
Chrome DevTools Protocol を話すブラウザを駆動する Go パッケージで、外部依存はありません。Selenium と違い driver バイナリは不要、Playwright と違いランタイムを同梱しません。Go バイナリと Chrome のインストールがデプロイの全部です。
chromedp で “invalid context” が出るのはなぜ?
Run なしで action を実行したからです。FAQ は chromedp context にデフォルトで executor がないと説明します。Actions は chromedp.Run が実行する値で、直接呼んでも実行するものがありません。
chromedp の “context canceled” は何を意味する?
たいていブラウザ接続が失われたことです。FAQ は手動で閉じたかプロセスが殺されたことに帰します。タイムアウトとの区別は価値があります。クラッシュした Chrome は長く待っても直りません。
見えるブラウザで chromedp を動かすには?
Chrome はデフォルトでヘッドレスです。chromedp.Flag("headless", false) を DefaultExecAllocatorOptions に足して NewExecAllocator に渡し、その allocator から context を派生させます。
chromedp でプロキシを使うには?
chromedp.ProxyServer("http://host:port") を allocator オプションに足します。URL 内の資格情報は効きません。Chrome の --proxy-server が受け付けないからです。アドレスが安定なら IP 許可リストを使い、そうでなければ DevTools Protocol で認証要求を扱います。
Docker で chromedp を動かすには?
Go プログラムを chromedp/headless-shell イメージ内で動かします。プロジェクトが明示的に推奨します。chromedp が設定なしで見つける小型ヘッドレス Chrome ビルドを含み、ブラウザ環境を手で組むことを避けます。
chromedp で要素を待つには?
セレクタ付きの WaitVisible、WaitReady、WaitNotVisible、またはセレクタが表せない条件には JavaScript 式の Poll。Sleep は避けてください。短すぎて不安定か長すぎて無駄で、通常はマシン次第で両方です。
chromedp 使用時に帯域を減らすには?
リクエスト傍受を有効にし、画像・フォント・メディアを失敗させてブロックすると、たいていトラフィックの大部分が消えます。ブラウザを1つ再利用しタブを作り、繰り返し割り当てない。内容が初期 HTML にあるならブラウザを完全に飛ばしてください。
まとめ
chromedp の学習曲線はほぼ全部が context モデルです。context がブラウザかタブを運び、キャンセルすると閉じ、actions は Run が実行するまで何もしない、と内化すれば、残りの API はまっすぐです。
大事な習慣はどのブラウザ自動化でも同じです。sleep ではなく条件を待ち、各待ちに context タイムアウトで上限を付け、起動コストを繰り返し払わず1つのブラウザを多くのタブで再利用します。
Go 固有の2点を覚えてください。すべての context で defer cancel()、さもなくば Chrome プロセスが漏れます。Linux では chromedp が起動した Chrome 子プロセスを強制終了するので、長時間動かすブラウザは別に起動し RemoteAllocator で到達します。
従量プロキシ経由なら、他の何より先に不要なリソース種別をブロックしてください。描画されたページは含まれる HTML より一桁高く、その大半は見るつもりのなかった画像です。
