chromedp là một thư viện thuần Go, giấy phép MIT, có khả năng điều khiển Chrome thật thông qua Chrome DevTools Protocol. Nó đọc DOM sau khi JavaScript của trang chạy xong, ngay trong chương trình Go, không cần WebDriver riêng hay runtime Node. Module Go được biên dịch trực tiếp vào ứng dụng, nhưng hệ thống chạy thực tế vẫn cần một file thực thi Chrome bên ngoài, và vòng đời của nó được quản lý bằng Go context.
Cài đặt khá gọn: chỉ một lệnh go get cộng thêm một bản Chrome mà tôi phải tự cung cấp. Tôi tạo allocator, suy ra context, rồi đưa danh sách action vào Run. Trên máy macOS arm64 này, với headless shell đã được làm nóng sẵn trên đĩa, một tiến trình mới từ lúc khởi chạy đến kết quả script đầu tiên có median 102 ms. Đây chỉ là số đo cục bộ, không phải khẳng định chung rằng khởi động luôn không phải nút thắt. Trên một fixture có thể chèn link sau 800 mili giây kể từ lúc load, có 2 trong 4 cách đọc trả kết quả trước khi link tồn tại.
Trình duyệt là thật, nội dung cũng có đó, nhưng code lại không đợi nó. Khoảng trống này là điều hữu ích nhất mà chromedp dạy tôi, và đó không phải bug — nó là ranh giới giữa “tôi đã render trang” và “tôi đã đợi đúng thứ mình cần”, một khác biệt mà các câu chuyện truyền miệng về trình duyệt headless thường làm phẳng đi. Chiến lược chờ, chứ không phải trình duyệt, mới quyết định bạn có lấy được dữ liệu hay không. Mọi con số ở đây đều đến từ fixture cục bộ do tôi kiểm soát, với ground truth đã được đăng ký trước mọi lần chạy, và các bảng tóm tắt thô nằm trong thư mục chromedp của repo benchmark của chúng tôi.
chromedp thực chất là gì
chromedp được thiết kế tối giản. Không có Selenium server. Không có lớp bọc WebDriver. Không có Node runtime ẩn phía sau. Chương trình Go của bạn mở một WebSocket tới phiên bản Chrome rồi giao tiếp trực tiếp qua CDP, về cơ bản cùng kiểu giao thức mà Puppeteer dùng, chỉ là không có lớp JavaScript ở giữa.
Repo này có 13.212 star và 178 issue đang mở khi tôi kiểm tra vào ngày 27/07/2026, dưới giấy phép MIT. Phiên bản tôi thử nghiệm là v0.16.0, cũng là tag mới nhất trong repo. Cần lưu ý để khỏi nhầm: trang Releases của GitHub vẫn hiển thị v0.15.1 (phát hành 2026-04-01) là đối tượng release mới nhất, trong khi go get github.com/chromedp/chromedp@latest lại phân giải sang v0.16.0. Ở đây Go modules và GitHub release object đã lệch nhau. Không hỏng, chỉ hơi khó chịu khi bạn cố xác định mình đang chạy bản nào.
Mô hình tư duy là Go context từ đầu đến cuối. Bạn tạo một allocator context (biết cách khởi động Chrome), suy ra browser context từ đó, rồi gọi chromedp.Run(ctx, actions...) với một danh sách Actions. Một context con của browser context là một tab mới. Hủy context thì thứ nó đại diện cũng biến mất. Nếu bạn từng làm concurrency trong Go, cảm giác này rất quen; còn nếu chưa, thì hướng dẫn bắt đầu web scraping bằng Go của chúng tôi sẽ dễ vào hơn godoc của chromedp.
Có một ranh giới nên nói rõ ngay từ đầu: chromedp cho bạn DOM đã render. Nó không tự biến DOM đó thành dữ liệu có cấu trúc. Phần bạn muốn lấy ra — field, bảng, giá — là code do bạn viết và bảo trì. Nó là driver, không phải framework scraper.
Cơ chế bên trong
Mọi thứ trong chromedp đều là một Action, và Run thực thi một lát cắt gồm chúng theo thứ tự trên một target. Navigate, Click, Evaluate, OuterHTML, WaitVisible — đều cùng một interface, đều ghép nối được, đều chỉ là lệnh CDP khoác kiểu Go. Sự đồng nhất này là quyết định thiết kế tốt nhất của thư viện, vì nó đặt lớp tiện ích và giao thức thô trên cùng một tầng.
Điều đó quan trọng, vì lớp tiện ích được giữ mỏng có chủ đích. chromedp được xây trên cdproto, một bộ binding Go được sinh tự động, có kiểu rõ ràng, bao phủ toàn bộ bề mặt DevTools Protocol, và chromedp godoc ghi chú cả hai lớp song song. Khi action tiện lợi không có sẵn, bạn xuống thẳng lời gọi domain — network.Enable(), page.CaptureScreenshot(), runtime.Evaluate() — ngay trong cùng một Run. Không có bức tường nào ngăn giữa “API đẹp” và “API thật”, điều mà không phải driver trình duyệt nào cũng làm được.
Các action chờ là nơi quyết định hằng ngày nằm ở đó, và có nhiều hơn số người thường dùng:
| Wait action | Nó chặn tới khi nào |
|---|---|
WaitReady(sel) | khi node được gắn vào DOM |
WaitVisible(sel) | khi node thực sự hiển thị |
WaitNotPresent(sel) / WaitNotVisible(sel) | trạng thái ngược lại, hữu ích cho spinner |
Poll(js, res) | kiểm tra một biểu thức JavaScript theo chu kỳ cho đến khi đúng |
Quản lý tiến trình là phần cơ chế khác đáng biết, vì nó quyết định chương trình của bạn có để lại Chrome chạy nền hay không. chromedp khởi động Chrome bằng exec.CommandContext của Go. Hủy context đó sẽ giết tiến trình. Chi tiết triển khai nhỏ này giải thích cả hành vi tốt lẫn “góc sắc” mà tôi gặp khi thử nghiệm.
Cài đặt là một binary Go cộng với một Chrome bạn phải tự cung cấp
go get github.com/chromedp/chromedp giải quyết sạch sẽ lên v0.16.0, không có drama, và cây phụ thuộc không có import cgo. Vì vậy câu “thuần Go, không phụ thuộc bên ngoài” bạn thường thấy là đúng — nhưng đúng cho module Go.
Nó không đúng cho runtime. chromedp điều khiển một Chrome bên ngoài, và nếu máy không có Chrome thì chạy sẽ lỗi ngay lập tức. Mọi phép đo của tôi đều cung cấp đúng executable thông qua chromedp.ExecPath, trỏ tới headless shell Chrome for Testing bản 151.0.7922.10. Đây không phải điểm trừ — muốn điều khiển trình duyệt thì phải có trình duyệt — nhưng “không có phụ thuộc bên ngoài” và “bạn phải ship một Chrome 155 MB cùng binary của mình” là hai câu chuyện triển khai rất khác nhau, và chỉ một trong hai xuất hiện trong README.
Một bẫy setup thứ hai đã làm tôi tốn thời gian và rất đáng biết trước khi viết code. Issue #1591 của chromedp báo rằng trình chạy go test của Go 1.25+ hủy NewExecAllocator giữa chừng khi đang khởi động; cùng code đó chạy bình thường dưới dạng binary đã biên dịch. Tôi build một binary probe bằng go build rồi chạy binary đó cho mọi phép đo thay vì dùng go test. Go ở đây là 1.26.5, macOS arm64. Nếu trải nghiệm chromedp đầu tiên của bạn là một file test chết trong lúc Chrome khởi động, thì hãy đọc issue này trước khi đổ lỗi cho code của bạn.
Thực hành: bốn cách đọc cùng một trang, hai cách ra kết quả rỗng

Fixture là một server cục bộ trên 127.0.0.1, phục vụ ba loại nội dung khác nhau chỉ ở thời điểm chúng đi vào DOM: một <a> tĩnh có sẵn trong bytes trả về, một <a> do <script> nội tuyến tạo ra trong lúc parse ban đầu, và một <a> do setTimeout tạo ra sau một số mili giây cấu hình được sau sự kiện load. Marker và href của hai link do script tạo ra được ghép từ các mảnh chuỗi trong JavaScript, nên không hề tồn tại một literal liên tục nào trong bytes được phục vụ. Vì vậy, một kết quả “found” chứng minh Chrome đã chạy JavaScript, chứ không phải ai đó chỉ đọc HTML.
Recall được tính bằng Python dựa trên ground-truth marker đã đăng ký trước, không phải ngay trong Go probe, nên probe không thể gian lận bằng cách biết trước đáp án. Mỗi chiến lược chạy 3 lần; tập kết quả found giống hệt nhau cả 3 lần.
| Chiến lược đọc | Link HTML tĩnh | Chèn trong lúc parse | Chèn sau load 800 ms | Thời gian |
|---|---|---|---|---|
Navigate + đọc ngay, không chờ | found | found | missed | 317 ms |
WaitReady("body") | found | found | missed | 107 ms |
WaitVisible("#delayed-injected") | found | found | found | 912 ms |
| Poll cho đến khi marker xuất hiện | found | found | found | 972 ms |
Hai dòng đầu chỉ lấy được 2 trong 3 link. Cách đọc ngây thơ bị hụt vì Navigate trả về theo sự kiện load, trong khi link thứ ba chưa tồn tại. WaitReady("body") hụt vì một lý do tinh vi hơn và nguy hiểm hơn trong thực tế: body đã được gắn vào DOM từ lúc load, nên điều kiện chờ được thoả ngay lập tức và bạn tưởng như mình đã làm đúng. Nó trả về sau 107 ms, còn nhanh hơn cả đường đi không chờ, nhưng kết quả vẫn là một trang không đầy đủ.
Để xác nhận cơ chế thay vì chỉ đoán, tôi quét độ trễ chèn và chạy lại hai cực trị (recall-summary.json):
| Độ trễ chèn sau load | Đọc không chờ có thấy không | WaitVisible có thấy không | Thời gian WaitVisible |
|---|---|---|---|
| 0 ms | có (race) | có | 109 ms |
| 100 ms | không | có | 208 ms |
| 400 ms | không | có | 519 ms |
| 800 ms | không | có | 911 ms |
| 1500 ms | không | có | 1625 ms |
Thời gian của WaitVisible bám theo độ trễ chèn trên fixture này — 100 đến 208, 400 đến 519, 800 đến 911, 1500 đến 1625 — cho thấy nó thực sự chặn cho đến khi node xuất hiện chứ không đọc quá sớm. Hàng 0 ms là ranh giới: setTimeout(…, 0) có thể bắn trước khi lệnh đọc tức thì chạy, nên đường không chờ vẫn có thể bắt được nó. Từ 100 ms trở lên trong sweep này, đường không chờ đã bỏ lỡ trong mọi lần chạy.
Trong production, sai lệch thời gian kiểu này có thể tạo ra HTML hợp lệ nhưng số dòng trích xuất bằng 0, rồi pipeline vẫn thoát 0 nếu không kiểm tra số lượng output. Đó là một chế độ lỗi hợp lý, được hành vi của fixture hỗ trợ, chứ không phải sự cố tôi đo trực tiếp ở đây. Render chỉ là một nửa yêu cầu; bước đọc phải đợi đúng điều kiện ở mức ứng dụng gắn với dữ liệu mong muốn.
WaitReady và WaitVisible không phải thứ hạng — chúng trả lời hai câu hỏi khác nhau
Cách diễn đạt phổ biến là WaitVisible “đáng tin hơn” WaitReady. Câu đó đủ mơ hồ để gây hại. Trên một trang có node đã gắn vào DOM nhưng được style display: none, hai thứ này tách biệt rất rõ (waitsem-summary.json, 3 lần chạy giống hệt nhau):
| Node mục tiêu | Action | Kết quả | Thời gian |
|---|---|---|---|
đã gắn, display:none | WaitReady | trả về | ~6 ms |
đã gắn, display:none | WaitVisible | timeout, context deadline exceeded | 4000 ms |
| node hiển thị | WaitVisible, query mặc định | trả về | 4–12 ms |
| node hiển thị | WaitVisible, ByID | trả về | 1–2 ms |
| node hiển thị | WaitVisible, ByQuery | trả về | 1 ms |
WaitReady nghĩa là đã được gắn vào DOM. WaitVisible nghĩa là đã hiển thị. Chọn nhầm thứ cần chờ thì либо lướt qua nội dung chưa bao giờ render xong, hoặc bị chặn hết timeout trên một node vốn dĩ sẽ không bao giờ visible. Bản thân hành vi deadline khá sạch — đúng context deadline exceeded sau đúng 4 giây, không treo, không zombie state — điều mà không phải driver nào cũng làm được.
Một bẫy đã được báo cáo nhưng tôi không tái hiện được. Issue #440 nói WaitVisible("#id") bị treo với query mặc định, nhưng điều đó không xảy ra trên v0.16.0 — query mặc định, ByID và ByQuery đều trả về bình thường với node visible trong mọi lần chạy. Không tái hiện được không có nghĩa là đã sửa xong: đây chỉ là một dạng selector trên một trang, chưa đủ để xóa bỏ issue.
Cái defer cancel() bạn bỏ quên đang đỡ cả mái nhà
Đếm tiến trình, không đếm return value. Mỗi lần kiểm tra vòng đời đều dùng một --user-data-dir riêng và đếm các tiến trình Chrome browser thực bằng pgrep, loại bỏ renderer con. Mỗi đường đi chạy 3 lần (lifecycle-summary.json).
| Đường thoát (macOS, mỗi loại 3 lần) | Điều gì xảy ra với tiến trình chrome-headless-shell được khởi tạo | Thời gian |
|---|---|---|
| Hủy context và allocator | biến mất | 13, 13 và 12 mili giây |
| Thoát tiến trình Go mà không hủy | sống sót sau khi chương trình của bạn kết thúc — trước probe không có browser process nào, sau khi probe thoát thì còn một; để lại orphan cả 3/3 lần | — |
Hủy là sạch, nhanh, đúng như exec.CommandContext hứa hẹn. (Mọi orphan đều bị harness kill cưỡng bức sau đó; máy chủ được dọn sạch.)
Đây là hành vi đã biết, có tài liệu, và chỉ áp dụng theo nền tảng — số đo là của tôi, không phải phát hiện mới. Tracker của chromedp đã nói về nó từ nhiều góc: #774 mô tả hiện tượng không thoát tương tự trên FreeBSD, #752 báo cáo Chromium process bị treo trên macOS, còn #562 và #1566 giải thích cơ chế. Phần tôi bổ sung là số lượng process và thời gian ở cả hai phía đối lập, điều mà các báo cáo định tính đó không có.
Bản thân cơ chế này là câu chuyện build tag đáng biết. Trong source v0.16.0, allocate_linux.go đặt Pdeathsig = SIGKILL cho child process, nên Linux có tín hiệu kernel-level parent-death. Còn allocate_other.go, thứ mà macOS biên dịch, lại biến lời gọi đó thành no-op. Không có tín hiệu tương đương trên darwin, nên không có gì giết Chrome khi chương trình của bạn thoát. Trong khi đó, phần godoc lại đọc như một cam kết chung — command mặc định “gửi SIGKILL tới mọi browser đang mở khi chương trình Go thoát” — còn phạm vi Linux chỉ nằm trong source gắn build tag mà bạn phải tự đọc. Gọi đó là lời tài liệu hứa quá tay thì hợp lý; gọi đó là bug của chromedp thì không.
Hệ quả thực tế thì vẫn như nhau: trên macOS, defer cancel() là thứ gánh toàn bộ trọng lượng. Bỏ nó đi là mỗi lần chạy để lại một browser process. Tôi không thử Linux, nên tôi không suy rộng kết quả orphan sang đó — source gợi ý Linux hành xử khác, nhưng gợi ý không phải phép đo.
Cold start, concurrency, và những thứ tẻ nhạt quyết định cách bạn deploy

102 ms là median từ một tiến trình mới, allocator, context, điều hướng localhost và Evaluate đầu tiên qua 5 tiến trình, dao động từ 98 đến 111 ms (coldstart-summary.json). Trên fixture macOS arm64 này với headless shell trên đĩa đã ấm sẵn, thời gian khởi động nhỏ hơn nhiều so với phần chờ nội dung trì hoãn. Container, filesystem lạnh, CI, serverless và điều hướng production không được đo.
Về concurrency, chromedp cho bạn hai kiểu — một browser với nhiều context con (tab), hoặc nhiều browser độc lập. Bốn lần điều hướng, mỗi kiểu 3 lần (concurrency-summary.json):
| Chế độ | Thời gian toàn cục (p50) | Khoảng | Số tiến trình Chrome browser đỉnh |
|---|---|---|---|
| Chung một browser, 4 context con | 214 ms | 209–219 | 1 |
| 4 browser riêng | 264 ms | 261–278 | 4 |
Kết quả đáng chú ý ở đây là số process: một Chrome browser process so với bốn process cho bốn lần điều hướng cục bộ rất đơn giản này. Khoảng thời gian toàn cục không chồng lấn nhau nhưng vẫn chỉ mang tính xu hướng, chưa phải benchmark throughput. RSS và PSS không được đo, nên thí nghiệm này không chứng minh được tiết kiệm bộ nhớ.
Probe cho đường lỗi nhỏ ở đây không đủ chi tiết để kết luận về độ bền: bản nháp không chỉ ra HTTP status, lỗi điều hướng, event, hay logic harness đã bộc lộ từng tình huống như thế nào. Hãy xem việc xử lý 500/dead-link là chưa được báo cáo cho đến khi API result chính xác và artifact thô được công bố.
Những thứ chưa thử, vì vậy nằm ngoài phạm vi của các con số này: hành vi vòng đời trên Linux, concurrency vượt quá N=4 hoặc có workload thực từng trang, chênh lệch bộ nhớ (tôi đếm process chứ không đếm RSS), network interception và request capture, và các báo cáo timeout WaitReady đang mở trong #168 và #1593 — chúng mô tả timeout ngắt quãng, còn cái tôi đo là ngữ nghĩa của wait, một câu hỏi khác. Một máy, một bản Chrome.
chromedp không phải driver Go CDP duy nhất trên bàn thử: rod đã đi qua đúng fixture, harness, host và bản Chrome đó trong cùng buổi, và có bài viết riêng.
Ưu và nhược điểm
Ưu điểm:
- Quyền truy cập CDP thật — action tiện lợi và lời gọi domain thô của
cdprotoghép được trong cùng mộtRun, nên bạn không chạm trần API. - Mốc cold cycle cục bộ 102 ms p50, với biên 98–111 ms trên fixture macOS đã thử.
- Hủy context sẽ reap Chrome trong khoảng 13 ms, ổn định, ở mọi lần chạy.
- Child context dùng chung một browser process cho N tab (1 process thay vì 4 process cho browser riêng).
- Tính xác định cao trong test — tập recall, ngữ nghĩa wait và kết quả vòng đời đều giống nhau qua 3 lần lặp.
- Xử lý deadline sạch:
WaitVisibletrên điều kiện không thể đạt trả vềcontext deadline exceededđúng 4 giây thay vì treo. - Module thuần Go (không cgo), giấy phép MIT; nhưng vẫn cần một Chrome executable riêng ở runtime.
Nhược điểm:
- Cần Chrome bên ngoài ở runtime; danh tiếng “không phụ thuộc” chỉ đúng cho module Go.
WaitReady("body")là một cái bẫy trông có vẻ đúng nhưng âm thầm bỏ lỡ nội dung sau load — nó trả về sau 107 ms với trang chưa đầy đủ.- Đường đọc ngây thơ
Navigate+ đọc ngay sẽ bỏ lỡ mọi thứ được chèn sau load khoảng ≥ ~100 ms, một cách deterministic, không báo lỗi. - Trên macOS, thoát mà không
cancel()sẽ để lại browser mồ côi (3/3 lần). Đây là hành vi đã biết, theo nền tảng, nhưng rất dễ vấp. - Cách viết trong godoc về SIGKILL khi thoát đọc như áp dụng cho mọi nơi, trong khi cơ chế thật chỉ nằm ở source được build tag cho Linux.
go testtrên Go 1.25+ có thể hủy lúc allocator khởi động (#1591); hãy build binary thay vì test trực tiếp.- Nó trả về DOM, không phải dữ liệu có cấu trúc — mọi field bạn muốn đều là logic parse do bạn tự viết và bảo trì.
- Tag mới nhất (v0.16.0) đi trước đối tượng GitHub Release mới nhất (v0.15.1), khiến việc kiểm tra version hơi rối trong chốc lát.
chromedp dành cho ai, và ai nên bỏ qua
Nếu dịch vụ của bạn đã viết bằng Go và bạn cần một trình duyệt thật bên trong nó, chromedp là lựa chọn gần như hiển nhiên. Không có process Node phải quản, không có WebDriver server phải giữ sống, chỉ một binary đã biên dịch cộng với Chrome mà bạn tự ship hoặc cài đặt. Mô hình context bám vào primitive concurrency của Go trực tiếp đến mức vòng đời browser được điều khiển bằng cùng kỷ luật defer như mọi thứ khác trong codebase của bạn. Và nếu bạn cần thứ mà API tiện lợi không bao phủ — event CDP network, hook vòng đời trang chính xác, các mẹo cấp protocol — bạn đi thẳng xuống cdproto mà không rời khỏi thư viện.
Nó cũng phù hợp khi bạn muốn kiểm soát rõ ràng việc chờ đợi. Các wait action là primitive, không phải heuristic; chúng làm đúng điều chúng nói, và đó là một điểm mạnh khi bạn chấp nhận rằng chọn đúng cái nào giờ là việc của bạn.
Hãy bỏ qua nó nếu team của bạn không viết Go — cam kết ngôn ngữ mới là chi phí thật, không phải thư viện. Hãy bỏ qua nó nếu bạn muốn trải nghiệm auto-wait đoán thay bạn, vì chromedp sẽ không đoán; nó chỉ làm chính xác thứ bạn yêu cầu rồi trả về trạng thái trang tại thời điểm đó. Hãy bỏ qua nó, hoặc chuẩn bị ngân sách nghiêm túc, nếu thứ bạn thực sự cần là record có cấu trúc chứ không phải DOM: mọi field đều là selector do bạn viết, test, và sửa khi site thay đổi. Và nếu yêu cầu chỉ là “lấy dữ liệu từ 500 URL này cho tôi”, thì dựng hẳn orchestration trình duyệt bằng Go là một đống cơ chế khá nặng cho bài toán đó. Hướng dẫn browser automation của chúng tôi sẽ giúp bạn thấy khi nào cơ chế đó đáng công và khi nào thì không.
Các lựa chọn thay thế, và chỗ đứng của stack của chúng tôi
Trong nhóm browser driver, rod cũng là một driver Go CDP, còn Playwright và Puppeteer là lựa chọn phía Node, được phân tích trong bài so sánh Playwright và Puppeteer. Cam kết chờ của chúng không thể thay thế cho nhau. Playwright auto-wait cho actionability trước nhiều thao tác; nhưng điều đó vẫn không cho nó biết khi nào dữ liệu ứng dụng đã thực sự đến xong sau thao tác. chromedp cho bạn các primitive chờ ở mức thấp hơn và để cả actionability lẫn điều kiện sẵn sàng ở mức ứng dụng cho người gọi quyết định. Nếu bạn đang khảo sát bức tranh rộng hơn, tổng quan các scraper mã nguồn mở mà chúng tôi đã thử sẽ bao gồm các crawler tĩnh và thư viện trích xuất ở phía bên kia của ranh giới này.
Bài liên quan: Đánh giá Browserless.
Một dịch vụ trích xuất được quản lý là một nhóm sản phẩm khác. Nó đổi quyền kiểm soát browser và selector lấy rendering và shaping schema được thuê ngoài. Cách này hữu ích khi đầu ra là record có cấu trúc thay vì DOM, trong khi chromedp phù hợp hơn khi browser phải nằm dưới quyền điều khiển của dịch vụ Go của bạn. Chúng tôi xây Thunderbit, một dịch vụ như vậy, nhưng không chạy nó trên fixture này; vì thế thí nghiệm này không ủng hộ bất kỳ so sánh tương đương nào về độ trễ, chất lượng trích xuất hay chi phí với chromedp.
Dùng thử Thunderbit để trích xuất dữ liệu web
Kết luận
Có nên dùng chromedp không? Có, nếu bạn viết Go và muốn một trình duyệt thật nằm dưới quyền kiểm soát của mình. Trên fixture cục bộ này, nó đạt kết quả script đầu tiên trong 102 ms p50, thu hồi Chrome sau khi hủy trong khoảng 13 ms, và dùng một browser process cho bốn tab chạy đồng thời. Đó là những quan sát có giới hạn, không phải cam kết hiệu năng phổ quát; sức hút bền vững nằm ở quyền truy cập CDP trực tiếp từ Go khi lớp tiện lợi đã đi đến giới hạn.
Chỉ cần định lượng kỳ vọng cho đúng, vì danh tiếng của nó thường thổi phồng hai điều. “Thuần Go, không phụ thuộc” mô tả module; ở runtime bạn vẫn đang ship và quản lý một binary Chrome. Và “dùng headless browser là sẽ lấy được nội dung động” chỉ đúng khi điều kiện chờ được gắn với node bạn thực sự muốn — một cách đọc ngây thơ và WaitReady("body") đều đã đưa cho tôi một trang thiếu nội dung được chèn sau load 800 ms, một cách âm thầm, lần nào cũng vậy. Trên macOS, defer cancel() không phải sở thích phong cách; bỏ nó đi là mỗi lần chạy bạn lại làm rò rỉ một browser, một hành vi đã biết theo nền tảng nhưng vẫn là vấn đề của bạn. Làm đúng ba điểm đó, chromedp là một trong những browser driver dễ đoán nhất mà tôi đã đo. Làm sai, nó sẽ lỗi trong im lặng, và đó là kiểu lỗi tệ nhất đối với một scraper.
Dùng thử Thunderbit để trích xuất dữ liệu web Get Started Free
Câu hỏi thường gặp
chromedp có thực sự thấy được nội dung do JavaScript render không?
Có — nhưng chỉ khi bạn chờ đúng node mình cần. Trên fixture mà một link được chèn 800 ms sau sự kiện load, Navigate rồi đọc ngay đã bỏ lỡ nó, WaitReady("body") cũng bỏ lỡ, trong khi WaitVisible trên node đó và một lần poll JavaScript đều lấy lại được, mỗi cách 3/3 lần. Khi quét qua các độ trễ, cách đọc không chờ đã bỏ lỡ node ở mọi mức từ 100 ms trở lên. Render là cần; chờ đúng mới làm nó đủ.
Khác nhau giữa WaitReady và WaitVisible là gì?
WaitReady chặn cho đến khi node được gắn vào DOM. WaitVisible chặn cho đến khi nó thật sự hiển thị. Trên một node đã gắn nhưng bị style display: none, WaitReady trả về sau khoảng 6 ms, còn WaitVisible chặn đến hết deadline 4 giây rồi trả về sạch với context deadline exceeded. Không có cái nào “đáng tin hơn” — chúng trả lời các câu hỏi khác nhau, và chọn nhầm cái mới là bẫy thật.
Tôi có thật sự cần defer cancel() với chromedp không?
Trên macOS, có. Hủy context và allocator sẽ reap Chrome được khởi tạo trong 12–13 ms ở mọi lần chạy; còn thoát tiến trình Go mà không hủy thì để lại browser mồ côi trong cả 3/3 lần. Đây là hành vi đã biết, theo nền tảng — tracker của chromedp ghi nhận cùng mẫu không thoát này trên các hệ không phải Linux, và cơ chế kill do parent-death nằm trong source chỉ build trên Linux. Tôi không thử Linux, nên hãy xem kết quả orphan là phạm vi macOS.
chromedp có cần cài Chrome riêng không?
Có. Bản thân module Go là thuần Go, không có cgo, nhưng nó điều khiển một browser bên ngoài và sẽ lỗi ngay nếu không có browser đó. Tôi đã chỉ rõ chromedp.ExecPath tới headless shell Chrome for Testing 151.0.7922.10. Điểm cộng là chi phí khởi động khá nhỏ: một cold cycle đầy đủ đến kết quả script đầu tiên có median 102 ms qua 5 tiến trình mới, dao động 98–111 ms.
Nên dùng chung một browser cho nhiều tab hay khởi động browser riêng? Nếu mục tiêu là giảm số lượng browser process, hãy ưu tiên child context. Bốn lần điều hướng localhost qua một browser chỉ dùng 1 Chrome browser process; bốn browser riêng dùng 4. Thời gian toàn cục cũng nghiêng về cách dùng chung (median 214 ms so với 264 ms), nhưng bốn trang local đơn giản không phải benchmark throughput. Bộ nhớ không được đo. Browser riêng vẫn có thể là lựa chọn đúng khi bạn cần cách ly session mạnh hơn, proxy khác nhau, hoặc muốn giảm phạm vi ảnh hưởng khi crash; những đánh đổi đó nằm ngoài thử nghiệm này.


