Browserless คือ Chrome แบบ headless ที่แพ็กมาเป็นบริการซึ่งคุณโฮสต์เองได้ ภายใน Docker container จะรันค้างไว้ รับงานผ่าน HTTP หรือ WebSocket และบังคับใช้โควตาการรับงานร่วมกัน แทนที่จะถูก import เข้าไปในแต่ละตัวเรียกใช้งาน ในการทดสอบ REST ที่นี่ บริการไม่มี Chrome process ค้างอยู่เลยตอน idle, มี Chrome process เกิดขึ้นระหว่างที่มีคำขอทำงาน และกลับไปเป็นศูนย์อีกครั้งหลังจบงาน สิ่งที่ถูกแชร์ร่วมกันคือความสามารถของบริการและคิวงาน ไม่ใช่ชุด browser processes ที่อุ่นเครื่องไว้ล่วงหน้าแบบยืนยันได้
ผมทดสอบ v2.55.0 กับ fixture ภายในที่ควบคุมได้ — แยกการเริ่มต้นเป็นหลายช่วง, ทดสอบ admission control 3 แบบ, ตรวจความตรงของ endpoint เทียบกับค่าจริงที่รู้แน่นอน, การ soak 30 session และขอบเขต timeout สิ่งที่ได้ประโยชน์จริงไม่ใช่ตัวเลขความเร็ว แต่เป็นพฤติกรรมเชิงการปฏิบัติงาน: ขีดจำกัดการรับงานตรงกับ response ที่ฝั่ง client เห็น และจุดเปราะส่วนใหญ่เป็นเรื่องของการ deploy มากกว่า
ผลลัพธ์ที่มีประโยชน์ที่สุดไม่ใช่เลข latency Browserless ไม่ได้ทำให้ Chrome เปิดเร็วขึ้น — แต่มันใส่นโยบายหน้าประตูให้ Chrome: มีจำนวน session จำกัดอยู่ข้างใน มีคิวต่อท้าย และใครเกินกว่านั้นก็โดน HTTP 429 เพดานนี้ขยับจาก 4 เป็น 8 เป็น 10 เมื่อผมเปลี่ยน environment variable สองตัว และการนับภายใน container ก็ตรงกับ status code ที่ client เห็นทุกคำขอ
Browserless คืออะไรกันแน่
หลายคนมักเข้าใจผิดตรงหมวดหมู่นี้ Browserless ไม่ใช่ไลบรารีที่คุณ import แล้วเรียกใช้ แต่มันคือ Docker image — ghcr.io/browserless/chromium — ที่คุณรันเป็นบริการระยะยาว มันทำหน้าที่เป็นตัวกลางจัดการงานเบราว์เซอร์ และเปิดให้ใช้งานได้ 2 ทาง: REST endpoints (/content, /scrape, /screenshot, /pdf รวมถึง /function และ /unblock) และ CDP/WebSocket surface ที่ Puppeteer กับ Playwright สามารถ connect() เข้าไปได้
ผมทดสอบเฉพาะ REST surface ส่วน WebSocket มีอยู่จริงและถูกใช้งานอย่างแพร่หลาย แต่ผมไม่ได้วัดมัน
เวอร์ชันที่ทดสอบคือ v2.55.0 ตรวจสอบเมื่อ 27 กรกฎาคม 2026
| รายการ | ค่า |
|---|---|
| เวอร์ชันอิมเมจ | v2.55.0, เผยแพร่ 14 กรกฎาคม 2026 |
| Chrome | 149.0.7827.0 |
| Node | 24.18.0 |
| Base image | Ubuntu 24.04 |
| GitHub stars | ประมาณ 13,525 ณ วันที่ 27 กรกฎาคม 2026 |
จำนวนดาวเปลี่ยนได้ตลอด ให้มองว่าเป็นการอ่านค่าตามช่วงเวลานั้น
ประตูเรื่องไลเซนส์: SSPL-1.0 หรือเชิงพาณิชย์
ใน repository ระบุว่า Browserless ใช้ได้ภายใต้ SSPL-1.0 หรือไลเซนส์เชิงพาณิชย์ของ Browserless โปรดอ่านไฟล์ repository LICENSE ฉบับปัจจุบัน และ แนวทางการ deploy open-source อย่างเป็นทางการของ Browserless ก่อนตัดสินใจเลือกเส้นทางใดเส้นทางหนึ่ง บทความนี้ไม่ได้วิเคราะห์เชิงกฎหมายเกี่ยวกับผลิตภัณฑ์เชิงพาณิชย์ แอป closed-source ระบบ CI บริการที่โฮสต์ไว้ หรือการใช้งานภายในองค์กร จึงไม่ได้จัดหมวดหมู่สถานการณ์เหล่านั้นตามไลเซนส์ ให้ฝ่ายกฎหมายหรือผู้รับผิดชอบด้านไลเซนส์ซอฟต์แวร์เป็นผู้ประเมินโมเดลการ deploy และการแจกจ่าย
Browserless ยังมีแพ็กเกจแบบโฮสต์ให้ซื้อด้วย ราคาและนิยามของหน่วยการใช้งานเปลี่ยนได้เสมอ และไม่ได้อยู่ในการทดสอบ self-hosted ครั้งนี้ ดังนั้นควรตรวจสอบจากเว็บไซต์ทางการแทนการอ้างอิงตารางเก่าที่นี่เพื่อใช้เป็นหลักฐานการจัดซื้อ
โมเดล session ทำงานอย่างไรเบื้องหลัง
เส้นทาง REST ที่วัดได้มีพฤติกรรมคล้ายการทำงานเบราว์เซอร์ต่อหนึ่งคำขอ แต่ harness นี้ไม่ได้เจาะลึกภายใน Browserless พอที่จะชี้ขาดได้ว่ามันเป็น browser process ใหม่ทุกครั้ง หรือเป็นกลยุทธ์ reuse context แบบใดกันแน่ สิ่งที่ยืนยันได้ง่ายกว่าคือ: ตอน idle ไม่มี Chrome process เลย, ระหว่างคำขอมี chrome-family processes 11 ตัว, และหลังรันแบบ sequential ก็กลับไปเป็นศูนย์ ไม่มีหลักฐานของ browser pool ที่อุ่นไว้ล่วงหน้าในคอนฟิกนี้
บริการ Node ที่รันค้างอยู่จะรับงานเบราว์เซอร์ได้ในจำนวนจำกัด, ต่อคิวได้อีกจำนวนหนึ่งที่จำกัด, แล้วปฏิเสธที่เหลือ ให้มองโมเดลการรับงานนี้เป็นสัญญาทางสถาปัตยกรรมที่วัดได้ด้านล่าง อย่าสรุปว่ามีการ reuse process หรือ context จากคำว่า “pool” เอง เพราะ timing harness นี้พิสูจน์ตรงนั้นไม่ได้
ตัวควบคุม admission มี 2 ตัว:
CONCURRENT— จำนวน session ที่รันพร้อมกันได้QUEUED— จำนวนคำขอเพิ่มเติมที่รอคิวได้
endpoint /config ของ container รายงานค่าเริ่มต้นเป็น CONCURRENT=10, QUEUED=10, TIMEOUT=30000 สิ่งที่เกินกว่า CONCURRENT + QUEUED จะถูกปฏิเสธทันที
การยืนยันตัวตนไม่ใช่ตัวเลือก Browserless v2 ต้องใช้ token เสมอ — ถ้าคุณไม่ตั้ง TOKEN มันจะสร้าง token แบบสุ่มแล้วพิมพ์ออก stdout ตอนเริ่มต้น ทุกคำขอ REST ต้องแนบ ?token= ไปด้วย
ด้าน observability คุณจะได้ /pressure (running, queued, CPU, memory, recently rejected), /sessions และ /config นอกจากนี้ยังมี export JSON สำหรับ /metrics แต่ต้องตั้ง METRICS_JSON_PATH ซึ่งผมไม่ได้ใช้ และ image นี้รัน dumb-init เป็น PID 1 ซึ่งเป็นคำตอบตามเอกสารต่อข้อร้องเรียนเรื่อง zombie process ที่ตามหลอก Chrome ใน container มาหลายปี
ความจริงของการตั้งค่า: คำสั่งเดียว แต่มี 4 เรื่องที่ไม่มีใครใส่ในคำสั่งเดียว
บรรทัดติดตั้งที่ทุกคนชอบอ้างคือ docker run แค่คำสั่งเดียวจริง ๆ แต่สิ่งรอบ ๆ มันคือสิ่งที่คุณควรวางแผนไว้
อิมเมจมีขนาด 4.34 GB ตัวเลขนี้ต่างหากที่ควรใช้กำหนดความคาดหวัง ไม่ใช่ startup latency manifest รองรับทั้ง linux/arm64 และ linux/amd64; บนโฮสต์ arm64 ของผม Docker ดึงตัว arm64 แบบ native มาให้ (แต่ user-agent ของ Chrome ภายใน container ยังอ่านว่า X11; Linux x86_64 — นั่นคือ UA cosmetic ของ Chrome บน Linux ไม่ใช่การ emulation uname -m บอกว่า aarch64 เรื่องนี้คนมักแจ้งบั๊กกัน)
ผมใช้ --shm-size=2g ทุกครั้งในการวัด harness นี้ไม่มีการรัน control บน /dev/shm ค่า default ของ Docker ดังนั้นบทความนี้จึงยังสรุปไม่ได้ว่า 2 GiB จำเป็นเสมอหรือชี้จุดล้มเหลวได้ตรงไหน ควรตั้งค่าตามจำนวนเบราว์เซอร์และ workload ของคุณ
token คือเรื่องของการ deploy ไม่ใช่พิธีการ ถ้าไม่มี token ใครก็ตามที่เข้าถึงพอร์ต 3000 ได้ก็สามารถควบคุม browser บนเครือข่ายของคุณได้
เครือข่ายของ container เป็นปัญหาของคุณ fixture ของผมรันบน host ดังนั้น container จึงเข้าถึงผ่าน host.docker.internal ได้ (colima map ไว้ด้วย --add-host host.docker.internal:host-gateway) ผมตรวจด้วย curl ดิบ ๆ ก่อนจะเชื่อผลวัดใด ๆ ว่า container เข้าถึง fixture ได้จริง
สภาพแวดล้อมของผม: colima 0.10.3 (6 CPU / 11.6 GiB) กับ Docker 29.2.1 บน macOS 26.5.2 arm64 harness ใช้แค่ Python 3 stdlib สำหรับ PNG และ PDF ผมตรวจแค่ file signature ไม่ได้ตรวจความถูกต้องของ decoder ขนาด จำนวนหน้า ความสมบูรณ์ หรือความตรงเชิงภาพ
การรันแบบเทียบเท่าอย่างย่อใช้ image ที่ pin ไว้, token ชัดเจน, และ shared-memory allocation แบบที่ใช้ที่นี่:
docker run --rm -p 3000:3000 --shm-size=2g \
-e TOKEN=replace-with-a-secret \
ghcr.io/browserless/chromium:v2.55.0
หลังจาก /pressure?token=... ตอบกลับแล้ว ให้ส่ง POST /content?token=... แบบ authenticated พร้อม body JSON ที่มี URL เป้าหมายเพื่อทดสอบเส้นทาง REST ฝั่ง production ควรมีกลไก retry แบบจำกัดและมี jitter สำหรับ response 429 ด้วย เพราะถ้าลองใหม่ทันที ก็แค่ไปแย่งคิวที่เต็มอยู่อีกครั้ง
ต้นทุนตอนเริ่มต้น แยกให้เห็นเป็นช่วง

การบูตใหม่จาก docker run 3 ครั้ง, ใช้ค่ามัธยฐานพร้อมช่วง min–max:
| ขั้นตอน | ค่ามัธยฐาน | ช่วง | มันคืออะไรจริง ๆ |
|---|---|---|---|
docker run → /pressure ตอบ 200 | 0.78 s | 0.70–0.87 s | endpoint HTTP พร้อมใช้งาน; การเปิด browser ยังไม่ได้ถูกยืนยันด้วยการเช็คนี้ |
พร้อมแล้ว → เรนเดอร์ /content ครั้งแรก | 0.32 s | 0.28–0.41 s | คำขอแรกที่สังเกตได้: browser ทำงาน + navigate + ส่ง HTML กลับมา |
คำขอ /content ครั้งถัดไป | 0.15 s | 0.147–0.154 s | latency ของคำขอถัดไปใน container เดียวกันที่สังเกตได้ |
แถวกลางเป็นจุดที่ตีความเกินได้ง่าย มันไม่ได้วัดการเปิด browser แบบแยกเดี่ยว และไม่ได้พิสูจน์ว่า Browserless เริ่ม Chrome ได้เร็วกว่าไลบรารีที่รันใน process เดียวกัน มันคือ HTTP round trip เข้าไปใน container บวกกับการทำงานของ browser, navigation และการส่ง response ประมาณ 0.17 วินาทีที่ต่างกันระหว่างครั้งแรกกับครั้งถัดไปอาจมาจาก filesystem, OS, Chrome, Node หรือ cache ของ container ก็ได้ เพราะตอน idle มี Chrome process เป็นศูนย์ และ harness นี้ไม่ได้เก็บ CDP trace หรือไทม์ไลน์ของ process สำหรับคำขอเหล่านี้ จึงไม่สามารถชี้ว่า gap นั้นเกิดจาก browser reuse หรือ cost ของการเปิดแบบ amortized
อีกอย่าง: ตัวเลขเหล่านี้มาจาก colima VM บน macOS บน bare-metal Linux จะต่างออกไป อย่าเอา 0.78 s ไปบอก SRE ของคุณราวกับใช้ได้ทุกที่
ภาคปฏิบัติ: หาเพดานจากทั้งสองฝั่ง
สัญญา CONCURRENT + QUEUED → 429 ถูกพูดซ้ำทุกที่ แต่แทบไม่เคยมีใครสาธิตให้เห็นจริง
วิธีทดสอบ: route ของ fixture จะ sleep ฝั่งเซิร์ฟเวอร์ 5 วินาที ทำให้แต่ละคำขอครอบครองหนึ่ง session อย่างแน่นอนตามระยะเวลาที่รู้ได้ จากนั้นยิงคำขอพร้อมกันจำนวน CONCURRENT + QUEUED + 4 แล้วดูว่าอะไรกลับมา — ในขณะที่มี sampler thread อีกตัวคอย polling /pressure เพื่ออ่านการนับภายในของ container เอง
| คอนฟิก (CONCURRENT, QUEUED) | ยิงทั้งหมด | HTTP 200 | HTTP 429 | จุดสูงสุดจาก /pressure ของ server (running / queued / recentlyRejected) |
|---|---|---|---|---|
| (2, 2) | 8 | 4 | 4 | 2 / 2 / 4 |
| (3, 5) | 12 | 8 | 4 | 3 / 5 / 4 |
| (5, 5) | 14 | 10 | 4 | 5 / 5 / 4 |
มี 3 เรื่องที่เห็นจากตรงนี้
เพดานคือ CONCURRENT + QUEUED เป๊ะทุกครั้ง response ที่สำเร็จมีจำนวน 4, 8 และ 10 ซึ่งเท่ากับผลรวมที่ตั้งไว้ในแต่ละกรณี ส่วนที่โดนปฏิเสธเท่ากับส่วนเกิน ซึ่งเป็น 4 ทุกครั้งในทั้ง 3 รัน
เพดานนี้ ขยับได้ มันไม่ใช่ค่าตายตัวที่ฝังมากับ image แต่มันคือค่าที่คุณกำหนด การขยับ 4 → 8 → 10 ด้วยการเปลี่ยน environment variable คือส่วนที่ทำให้มันมีประโยชน์จริง ไม่ใช่แค่เรื่องเล็กน้อย
และสัญญาณทั้งสองเป็นคนละเรื่องกัน status code ฝั่ง client ของผมมาจาก HTTP response จริง ๆ ส่วน /pressure มาจากการนับภายในของ container เองโดย thread อีกตัวหนึ่ง ทั้งสองสอดคล้องกันใน 3 รันสั้น ๆ นี้ นั่นทำให้ /pressure เป็นตัวชี้วัดที่น่าใช้ใน production ได้ แต่ยังไม่ใช่สัญญา autoscaling ที่สมบูรณ์: ยังต้องตรวจ cadence การ scrape, semantics ของการ reset, การรวมผลจากหลาย replica และพฤติกรรมภายใต้ workload ผสมที่นานกว่านี้
มีข้อสังเกตหนึ่งที่การนับผ่าน/ไม่ผ่านซ่อนอยู่ คำขอที่เข้าคิวไม่ได้ล้มเหลว — มันรอ และบางทีก็รอนาน ที่ (2, 2) กับงาน 5 วินาที response ที่สำเร็จมาถึงได้ตั้งแต่ 5.7 s ถึง 11.0 s ค่ามัธยฐาน 8.3 s ดังนั้น latency ปลายทางจึงยืดไปได้ประมาณสองรอบของระยะเวลาหนึ่ง session harness นี้ไม่ได้แยก timestamp ของการรับงานกับการทำงานออกจากกัน จึงไม่สามารถบอกได้ว่าความหน่วงทั้งหมดคือเวลารอคิว
มองสิ่งนี้ในงานจริง
สมมุติว่าคุณต้อง render หน้า product 4,000 หน้าเป็น PDF ทุกคืน และแต่ละหน้าต้องใช้เวลาประมาณ 5 วินาที คุณตั้ง CONCURRENT=5, QUEUED=5 เพดาน throughput ของคุณคือ 5 หน้า ต่อ 5 วินาที — หรือ 1 หน้า ต่อวินาที — ดังนั้นงานทั้งหมดจะใช้เวลาประมาณ 67 นาที ถ้าคุณให้ท่อทำงานเต็มตลอด นี่เป็นคณิตศาสตร์จากพฤติกรรมที่วัดได้ ไม่ใช่ benchmark แต่เป็นคณิตศาสตร์ที่คุณควรทำก่อน deploy
คำขอใดก็ตามที่มาถึงหลังจากช่อง running และ queued ถูกใช้หมดแล้ว อาจโดน 429 ได้ทันที การยิงพร้อมกันไม่ได้รับประกันว่าคำขอใดตามลำดับจะเป็นฝ่ายแพ้ ผู้ควรเขียน job runner ให้มอง response นี้เป็น backpressure และ retry แบบมีขอบเขตพร้อม jitter ไม่เช่นนั้นอาจเสี่ยงทำให้บางหน้าไม่ถูกประมวลผล ในขณะที่การนับงานระดับบนยังเดินต่อไป — ความเสี่ยงเชิงปฏิบัติการ ไม่ใช่ failure scenario ที่ harness นี้พิสูจน์ให้เห็น
ภาคปฏิบัติ: endpoint มองเห็นอะไรจริง ๆ
เพื่อทดสอบความตรงของการเรนเดอร์อย่างซื่อสัตย์ หน้า fixture ซ่อน marker text จากใครก็ตามที่ไม่ได้ใช้ browser จริง ข้อความที่เห็น Runtime Injected Marker 88 ถูกประกอบขึ้นจาก JavaScript fragments ตอนโหลด ดังนั้นจึงไม่มี literal ต่อเนื่องของมันอยู่ในไบต์ที่เซิร์ฟเวอร์ส่งออกมา การดึงหน้าแบบ static ธรรมดาจะได้ 702 bytes ซึ่งไม่มี marker ทั้งสอง
| Endpoint | ผลลัพธ์ | Bytes |
|---|---|---|
/content | marker ที่ inject ตอนรัน ปรากฏ พร้อม marker แบบ static ทั้งคู่ | 811 |
/scrape บน #scrape-me (node ที่ inject ด้วย JS) | ได้ค่า SCRAPE_TARGET_VALUE_CC | 422 |
/screenshot | response ที่มี PNG signature 89 50 4E 47 | 18,621 |
/pdf | response ที่มี PDF signature %PDF- | 40,974 |
| ทั้ง 4 รายการเมื่อไม่มี token | HTTP 401 (ไม่ใช่ 403) | — |
การที่ /content ส่งกลับ 811 bytes พร้อม injected marker แปลว่า Chromium จริงได้เรนเดอร์หน้าก่อนที่ HTML จะถูกส่งกลับมา /scrape ดึงค่าจาก node ที่ไม่มีอยู่จนกว่าจะมี JavaScript ทำงาน ทั้งสองอย่างสำเร็จด้วย โค้ด automation ฝั่ง client เป็นศูนย์ — แค่ POST ที่ authenticated ครั้งเดียว
นั่นคือสิ่งที่มันขายจริง ๆ ในรอบทดสอบเดียวกัน static crawler พลาด content ประเภทนี้ไปทั้งหมด ส่วน browser library ที่รันใน process เดียวกัน (chromedp, rod, Selenium) จะจับได้ก็ต่อเมื่อผมเขียน explicit wait เพิ่มเอง Browserless จับได้ด้วย request หน้าตาเหมือน curl คุณกำลังแลก automation code กับภาระด้าน deployment
มี 2 ขอบเขตของข้อความนี้ หลักฐานครอบคลุม content class ใน fixture ของผม ไม่ใช่การสำรวจเว็บสมัยใหม่ทั้งหมด และ /unblock ซึ่งเป็น endpoint สำหรับ anti-detection ผมตั้งใจไม่แตะเลย — ผลลัพธ์เหล่านี้จึงไม่ควรถูกตีความว่าเป็นการอ้างความสามารถด้าน anti-bot /function, /download และ /performance ก็ไม่ได้ทดสอบเช่นกัน
ภาคปฏิบัติ: ตรวจ residue แบบสั้น ๆ
Chrome ใน container มีชื่อเสียงเรื่องทิ้งซากไว้ ผมจึงรัน session แบบต่อเนื่อง 30 ครั้งด้วย CONCURRENT=3 และนับ process ภายใน container
ก่อนจะเชื่อสิ่งใด ผมคาลิเบรตตัวตรวจจับก่อน ขณะที่มี session กำลังทำงาน ตัว enumerator ใน /proc อ่านได้ 11 chrome-family processes (browser, zygote, GPU, renderer, utility) ซึ่งสำคัญมาก: มันพิสูจน์ว่าเครื่องมือมองเห็น Chrome ได้ ดังนั้นเลขศูนย์หลังจบรันจึงเป็นการวัด ไม่ใช่การมองไม่เห็น การทดสอบ leak ที่รายงาน “0 processes” โดยไม่พิสูจน์ว่ามันนับได้จริง ไม่มีค่าอะไร
หลัง 30 session: 0 chrome processes, 0 zombies สิ่งที่เหลืออยู่คือ dumb-init, node, Xvfb, start.sh และ sh เท่านั้น /sessions อ่านค่าได้ 0 ตอน idle
หน่วยความจำของ container จาก docker stats (คือตัวเลขที่ operator เห็น ไม่ใช่ RSS ของ process เดียว):
| หลัง N sessions | 0 | 5 | 10 | 15 | 20 | 25 | 30 |
|---|---|---|---|---|---|---|---|
| หน่วยความจำของ container (MiB) | 294 | 300 | 301 | 302 | 302 | 303 | 303 |
การเติบโตรวมตลอด 30 session อยู่ที่ประมาณ 9.5 MB และกราฟตัวอย่างเริ่มแบนหลัง session ที่ 10 นี่ไม่สอดคล้องกับการรั่วแบบเส้นตรงต่อ session ในช่วงสั้น ๆ นี้ การ warmup ของ Node เป็นคำอธิบายที่เป็นไปได้ แต่ไม่ใช่สิ่งที่ตัวเลข process และ memory ชุดนี้พิสูจน์ได้
ขอบเขต: 30 session แบบต่อเนื่องเป็นการ soak ระยะสั้น ไม่ใช่ endurance test หรือการรันท่ามกลาง concurrency ปัญหา EventEmitter listener warning ที่ค้างมานานใน issue tracker เป็นชนิดที่อาจโผล่เมื่อรันเป็นชั่วโมงหรือเป็นพัน session ซึ่งผมไม่ได้ทดสอบ สิ่งที่สรุปได้สนับสนุนเพียงว่าในช่วงหน้าต่างนี้บน v2.55.0 ไม่พบการสะสมของ Chrome process หรือ zombies
ขอบเขตของ timeout
TIMEOUT ถูกระบุว่าเป็น knob ที่ปรับได้ ผมอยากเห็นมันทำงานจริง
| กรณี | ค้างหน้า | สถานะ | เวลาที่ใช้ |
|---|---|---|---|
| ไม่เกินงบ | 2,000 ms | 200 | 2.406 s |
| เกินงบ | 15,000 ms | 408 | 5.007 s |
เมื่อ TIMEOUT=5000, session ที่พยายามค้างหน้าไว้ 15 วินาทีจะตอบ HTTP 408 ที่ 5.007 s แทนที่จะค้างไม่จบ การสังเกตเพียงครั้งนี้ยืนยันว่ามีการบังคับใช้ใกล้กับขอบเขตที่ตั้งไว้ แต่มันยังไม่เปิดเผยวิธีทำงานของ timer หรือพิสูจน์การคืนสล็อตอย่างสมบูรณ์ การทดสอบที่แข็งแรงกว่านี้ต้องรันซ้ำ, สังเกต /sessions และ /pressure กลับสู่ idle แล้วจึงยืนยันว่าคำขอถัดไปได้สล็อตที่เพิ่งปล่อยคืน
กับดักเวลาย้ายระบบ: PREBOOT ไม่ทำงาน แต่ก็ไม่บอกคุณ
จากทุกอย่างที่ผมวัด เรื่องนี้คือผลลัพธ์ที่ผมอยากมีคนบอกก่อนอัปเกรดที่สุด
Browserless 2.0.0 ลบ PREBOOT และ KEEP_ALIVE ออก — changelog ระบุว่าถูกตัดทิ้งเพราะสับสน ทำอะไรได้น้อย และก่อบั๊ก การตัดสินใจนั้นสมเหตุสมผล ปัญหาคือเมื่อคอนฟิก v1 ถูกคัดลอกมาวางบน v2 ซึ่งเป็นวิธีอัปเกรดที่คนส่วนใหญ่ทำกัน
ผมรัน container ด้วย -e PREBOOT=true แล้ววัดเทียบกับค่า default:
| สัญญาณ | PREBOOT=true | ค่า default, ไม่ตั้ง flag |
|---|---|---|
| เวลา ready | 0.716 s | 0.776 s |
| Cold render | 0.314 s | 0.318 s |
| Warm render | 0.163 s | 0.150 s |
| Chrome processes ตอน idle | 0 | 0 |
ทุก timing อยู่ภายในช่วง min–max ของฝั่ง default เอง — นั่นคือ noise ไม่ใช่ผลของมัน และไม่มีอะไรถูก pre-warm เลย: container ที่ตั้ง PREBOOT=true ตอน idle ก็ไม่ได้ถือ browser ไว้แม้แต่ตัวเดียว เหมือนกับ container ที่ไม่ได้ตั้ง flag นี้ มีสัญญาณอีก 2 อย่างที่ไม่ใช่ตัวเลข:
/configไม่ได้แสดง keyprebootเลย มีแค่concurrent,queued,timeout,token,maxCPU,maxMemory,retriesและอื่น ๆ- ไม่มี error ไม่มี warning และไม่มีอะไรใน container logs
ดังนั้นคอนฟิก PREBOOT จาก v1 เมื่อมาอยู่บน v2 จึงเป็น no-op เงียบ ๆ สำหรับการเริ่มต้นปกติและการเช็ค log สัญญาณที่ตรวจจับได้คือ key ที่หายไปใน /config บวกกับพฤติกรรมที่ไม่เปลี่ยนแปลง Browserless ไม่ได้ emit การปฏิเสธหรือ warning อย่างชัดเจน การตรวจ migration จึงต้องดูคอนฟิกที่ถูกนำไปใช้จริง ไม่ใช่คิดว่า startup สีเขียวแปลว่าทุก environment variable ถูกใช้แล้ว
KEEP_ALIVE เป็นกรณีตรงข้าม และไม่ควรเอามาปนกัน มันถูกลบใน release เดียวกัน แต่ ไม่ใช่ แบบเงียบ ๆ — การ probe แบบสั้น ๆ แสดงว่ามัน log ข้อความ Environment variable of "KEEP_ALIVE" is deprecated and ignored. ออกมาทาง stdout ทันที นั่นคือ warning ที่เหมาะสมสำหรับ operator ผมไม่ได้เอา KEEP_ALIVE เข้า harness แบบวัดผลเดียวกับ PREBOOT ดังนั้นจึงรายงานไว้ในฐานะการตรวจสอบ ไม่ใช่ผลการวัด แต่ทิศทางมันชัดพอที่จะมีความหมาย: มีแค่ PREBOOT เท่านั้นที่เป็นกับดักเงียบ ๆ Browserless ซื่อตรงกับ KEEP_ALIVE มากกว่าที่สรุปแบบกว้าง ๆ ว่า "v2 ไม่สนใจ flags ของ v1" จะให้เครดิตไว้
ข้อดีและข้อเสีย
ข้อดี
- การควบคุม admission ทำงานตรงตามเอกสารและขยับตามคอนฟิกได้ — พิสูจน์ที่ 3 เพดานต่างกัน พร้อมกันทั้ง status code ฝั่ง client และการนับภายในของ server
/pressureตรงกับจำนวน running, queued และ rejected ที่ client เห็นใน 3 รันสั้น ๆ; ใช้เป็นหนึ่งในสัญญาณสำหรับ autoscaling และ alerting ได้- เรนเดอร์ด้วย Chromium จริงโดยไม่ต้องมี automation code ฝั่ง client:
POSTที่ authenticated ครั้งเดียวก็เผย DOM ที่ inject ด้วย JS ซึ่ง static fetch มองไม่เห็น - ไม่พบ Chrome process สะสมในการรันต่อเนื่อง 30 session; หลังจบรันมี Chrome process 0 ตัวและ zombies 0 ตัว
- การทดลอง
TIMEOUTครั้งหนึ่งตอบ 408 ที่ 5.007 s เมื่อเทียบกับงบ 5.000 s; ยังไม่ได้แยกตรวจการ cleanup และการคืนสล็อต - เปิด auth มาตั้งแต่ต้น: ทั้ง 4 REST endpoints ตอบ 401 หากไม่มี token
- ใช้
docker runครั้งเดียวแล้วได้บริการที่พร้อมใช้งานในราว 0.78 s และเรนเดอร์ครั้งแรกหลังจากนั้นอีก 0.32 s
ข้อเสีย
- อิมเมจ 4.34 GB นี่คือต้นทุนจริงที่เห็นชัด และมันจะไปอยู่ทั้งใน registry, CI cache และเวลา cold-deploy
- SSPL-1.0 หรือไลเซนส์เชิงพาณิชย์ของ Browserless ต้องประเมินเงื่อนไขปัจจุบันให้ตรงกับโมเดล deploy และการแจกจ่ายของคุณ
PREBOOTจาก v1 ถูกยอมรับแต่ถูกเพิกเฉยแบบเงียบ ๆ บน v2 — ไม่มี error, ไม่มี warning, ไม่มี key ใน/config- คุณกำลังดูแลบริการ ไม่ใช่แค่เพิ่ม dependency: มีทั้ง container, token, เส้นทางเครือข่าย, เพดานการรับงาน และความรับผิดชอบเรื่องอัปเกรด
- คำขอที่เข้าคิวทำให้ latency ปลายทางยืดไปถึงประมาณสองช่วง session ในรัน
(2, 2); ยังไม่ได้วัดเวลารอคิวแยกต่างหาก - timing ของ REST ที่วัดได้รวม HTTP hop ไว้ด้วย และไม่ได้แยกต้นทุนการเปิด browser หรือพิสูจน์การ reuse ของ browser
ใครควรใช้ และใครไม่ควรใช้
Browserless คุ้มค่าจริงเมื่อ มีมากกว่าหนึ่งงานที่ต้องใช้ browser บริการเรนเดอร์ร่วมกันหลายแอป ทีมที่อยากได้ screenshot และ PDF ผ่าน HTTP endpoint แทนการฝัง dependency Chrome ไว้ในทุก service, หรือ pipeline งานที่ต้องมีเพดาน capacity พร้อม backpressure ที่วัดได้ — นี่คือรูปแบบที่มันเหมาะ ถ้าคุณมี Docker อยู่แล้วและมีคนรับผิดชอบการ deploy เรื่องปฏิบัติการก็ชัดเจน: admission ที่คาดเดาได้, backpressure ที่สังเกตได้, และไม่พบ Chrome process หรือ zombies สะสมในการตรวจแบบต่อเนื่อง 30 session
มันยังเหมาะถ้าทางเลือกอื่นคือให้ทุก service ใน stack ของคุณติดตั้ง Chromium ของตัวเอง การรวมศูนย์สิ่งนั้นไว้ใน container เดียวพร้อม token และเพดาน เป็นการตัดสินใจเชิงสถาปัตยกรรมที่ดีอย่างแท้จริง
ควรข้ามมันถ้าคุณกำลังเขียนสคริปต์แค่ตัวเดียว การดึง 4.3 GB และรัน container เพื่อให้ไฟล์ Python เดียวไปดึงหน้า render มันออกมา เป็นพิธีการที่เยอะเกินไปสำหรับงานเล็ก ๆ — browser library ใน process ของคุณเองทำได้โดยไม่ต้องมี service แยก ควรข้ามถ้าเงื่อนไข SSPL ไม่เหมาะกับผลิตภัณฑ์เชิงพาณิชย์ของคุณและแก้ไม่ได้ ควรข้ามถ้าสิ่งที่คุณต้องการจริง ๆ คือ browser ที่อุ่นมาแล้วโดยไม่มี cold cost เพราะ PREBOOT จะไม่ช่วยให้คุณได้แบบนั้นใน v2 และควรข้ามถ้าปัญหาหลักของคุณคือการจัดการ anti-bot เพราะมันอยู่ใน endpoint ที่ผมตั้งใจไม่ทดสอบ และจะไม่รับรอง
ทางเลือกอื่น รวมถึง Thunderbit อยู่ตรงไหน
การเทียบที่คุ้มค่าที่สุดไม่ใช่ Browserless กับ container อื่น แต่มันคือเบราว์เซอร์อยู่ที่ไหน และใครเป็นคนรับผิดชอบให้มันรันต่อเนื่อง
รีวิวที่เกี่ยวข้อง: Browsertrix Crawler review.
รีวิวที่เกี่ยวข้อง: chromedp review.
| ไลบรารีเบราว์เซอร์ (chromedp, rod, Selenium, Playwright) | Browserless self-hosted | Thunderbit managed extraction | |
|---|---|---|---|
| เบราว์เซอร์รันที่ไหน | ใน process ของคุณ | ใน container ของคุณ | บนโครงสร้างพื้นฐานของผู้ให้บริการ |
| ต้นทุนการตั้งค่า | ติดตั้งแพ็กเกจ | อิมเมจ 4.3 GB + container + token | API key |
| เวลาที่วัดในบทความนี้ | ไม่ได้วัดในบทความนี้ | เรนเดอร์ครั้งแรกหลัง HTTP readiness 0.32 s; คำขอถัดไปมัธยฐาน 0.15 s | ไม่ได้วัดในบทความนี้ |
| สิ่งที่คุณต้องเขียน | โค้ด automation พร้อม explicit wait | POST ที่ authenticated ครั้งเดียว | เรียก HTTP ครั้งเดียว |
| ผลลัพธ์ที่ได้กลับมา | แล้วแต่คุณจะสคริปต์ | HTML, response PNG/PDF ที่เทียบ signature, nodes ที่ scrape ได้ | JSON ที่มีโครงสร้างหรือ Markdown ตามประเภทงาน |
| เพดาน capacity | เครื่องของคุณ | CONCURRENT + QUEUED แล้วตามด้วย 429 | แผนบริการของผู้ให้บริการ |
| ใคร on call | คุณ | คุณ | ผู้ให้บริการ |
ถ้าคุณต้องการ browser อยู่ใน process ของตัวเองและไม่ติดการเขียน waits ไลบรารีจะเบากว่าและไม่ต้อง deploy บริการแยก ผมเคยเขียนไว้ด้านนั้นใน Playwright versus Puppeteer comparison และ open-source scraping project roundup ที่ครอบคลุมภาพกว้างกว่า
ถ้าคุณไม่ต้องการดูแล browser เองเลย Thunderbit ของเราก็เป็นอีกทางเลือกแบบ managed Browserless ส่งคืนสิ่งที่ render แล้วให้โค้ดของคุณตีความ ส่วน Thunderbit สามารถส่ง Markdown หรือข้อมูลที่ match schema ได้ โดยผู้ให้บริการเป็นคนรับผิดชอบโครงสร้างพื้นฐานการเรนเดอร์ บทความนี้ไม่ได้ benchmark latency, capacity, failure behavior, คุณภาพการ extract หรือค่าใช้จ่ายของ Thunderbit ดังนั้นตารางนี้จึงพูดถึงขอบเขตความรับผิดชอบ ไม่ใช่การเปรียบเทียบประสิทธิภาพ
อ่านต่อที่เกี่ยวข้องจากรอบการทดสอบเดียวกัน: Crawl4AI review ครอบคลุม pipeline Markdown ที่ใช้ browser และคุณเป็นคนรันเอง ส่วน web scraping tools overview จะช่วยวางภาพรวมของหมวดหมู่นี้
ลองใช้ Thunderbit สำหรับการดึงข้อมูลเว็บ
คำตัดสิน
ควรรัน Browserless ไหม? ควร ถ้ามีหลาย caller ที่ต้องใช้ browser, มีคนดูแล container ได้, และการตรวจไลเซนส์ของคุณอนุมัติรูปแบบการ deploy นี้ ในการทดสอบ admission แบบสังเคราะห์ 3 แบบ จำนวนที่รับได้ตรงกับ CONCURRENT + QUEUED, ส่วนเกินโดน 429 และ /pressure ตรงกับจำนวนที่ client เห็น ในการตรวจ sequential 30 session แยกต่างหาก ไม่พบการสะสมของ Chrome process หรือ zombies และการทดลอง timeout ครั้งหนึ่งก็คืน 408 ใกล้ขอบเขตที่ตั้งไว้ นั่นคือข้อสังเกตที่มีประโยชน์ภายใต้เงื่อนไขจำกัด ไม่ใช่คำรับรองสากล
แต่ให้ประเมินภาระให้ตรงไปตรงมาด้วย มันคืออิมเมจ 4.34 GB และบริการที่คุณต้องดูแล ไม่ใช่ dependency ที่แค่เพิ่มเข้าไป การทดสอบนี้ยังไม่ได้พิสูจน์ว่าเร็วกว่า browser library ที่รันใน process เดียวกัน สิ่งที่มันให้คือ browser ที่คุณสามารถ ปันส่วน ได้: เพดานที่รู้ได้แน่และ backpressure ที่วัดได้ ในการตรวจ sequential 30 session ไม่พบ Chrome process หรือ zombies สะสม ต้นทุนคือภาระด้าน deployment และไลเซนส์ที่คุณต้องอ่านเอง ถ้าคุณแค่เรนเดอร์ไม่กี่หน้าในสคริปต์เดียว ข้อตกลงนี้ไม่คุ้ม แต่ถ้าคุณกำลังรันชั้นเรนเดอร์ที่หลาย service ต้องพึ่งพา มันคุ้ม — แค่ตรวจ environment variable แบบ v1 ของคุณก่อนเข้าใช้ เพราะ PREBOOT จะนั่งอยู่ตรงนั้นเหมือนทำงานอยู่ ทั้งที่จริงไม่ทำอะไรเลย
ลองใช้ Thunderbit สำหรับการดึงข้อมูลเว็บ Get Started Free
คำถามที่พบบ่อย
Browserless ทำให้ headless Chrome เร็วขึ้นไหม?
การทดสอบนี้ตอบไม่ได้ HTTP endpoint พร้อมใช้งานหลัง docker run 0.78 s; คำขอ /content ครั้งแรกใช้เวลา 0.32 s และคำขอถัดไปใน container เดียวกันใช้ราว 0.15 s ตัวเลขเหล่านี้รวม HTTP round trip, งานของ browser, navigation และการส่ง response harness นี้ไม่ได้แยกเวลาการเปิด browser, ไม่ได้ trace การ reuse process และไม่ได้เผย benchmark แบบ in-process ที่เทียบกันได้ ใช้ Browserless เพื่อแบ่ง boundary ของบริการและการควบคุม admission แล้วค่อย benchmark เส้นทาง latency ของคุณเอง
เกิดอะไรขึ้นเมื่อเกินขีดจำกัด concurrency ของ Browserless?
คุณจะได้ HTTP 429 ทันที เพดานคือ CONCURRENT + QUEUED แบบเป๊ะ และผมยืนยันแล้วที่ 3 คอนฟิก: (2,2) รับได้ 4 และปฏิเสธ 4, (3,5) รับได้ 8 และปฏิเสธ 4, (5,5) รับได้ 10 และปฏิเสธ 4 endpoint /pressure ของ server รายงานจำนวน running, queued และ recentlyRejected ตรงกันทุกครั้ง สิ่งที่ควรรู้คือคำขอที่เข้าคิวไม่ได้ล้มเหลว — มันรอ — ที่ (2,2) กับงาน 5 วินาที response ที่สำเร็จใช้เวลา 5.7 s ถึง 11.0 s ออกแบบ client ของคุณให้มอง 429 เป็น backpressure พร้อม retry และ backoff
PREBOOT ยังใช้ได้ใน Browserless v2 ไหม?
ไม่ได้แล้ว PREBOOT ถูกเอาออกใน 2.0.0 และ v2 ยอมรับ -e PREBOOT=true โดยไม่ error หรือ warning แต่ก็ไม่ทำอะไรกับมัน ผมยืนยันความไม่ทำงานนี้ 3 ทาง: latency ไม่ต่างจาก default, container ที่ตั้ง PREBOOT=true ตอน idle ไม่มี chrome processes ค้างอยู่เลย และ /config ก็ไม่มี key preboot หากคุณย้ายคอนฟิก v1 มา instance ของคุณจะไม่ได้ถูกอุ่นไว้ล่วงหน้า สังเกตว่า KEEP_ALIVE ซึ่งถูกลบใน release เดียวกัน จะ log warning ว่า "deprecated and ignored" — ดังนั้นปัญหาการล้มเหลวแบบเงียบเป็นเรื่องเฉพาะของ PREBOOT
Browserless ใช้เชิงพาณิชย์ฟรีได้ไหม? ใน repository มีให้เลือก SSPL-1.0 หรือ Browserless commercial license แต่บทความนี้ไม่ได้จับคู่สถานการณ์เชิงพาณิชย์หรือ closed-source เฉพาะเจาะจงกับตัวเลือกใดตัวเลือกหนึ่ง โปรดตรวจ LICENSE ฉบับปัจจุบันและ official deployment guidance จากนั้นให้ผู้รับผิดชอบด้านไลเซนส์ซอฟต์แวร์ประเมินโมเดลการ deploy และการแจกจ่ายของคุณ
Browserless ทิ้ง Chrome zombie process ไว้ไหม?
ไม่พบการสะสมในช่วงที่ทดสอบ หลัง 30 session แบบต่อเนื่อง container มี Chrome process 0 ตัวและ zombies 0 ตัว โดยเหลือเพียง dumb-init, node, Xvfb, start.sh และ sh เท่านั้น ตัวตรวจนับเห็น chrome-family processes 11 ตัวในขณะที่ session ยังรันอยู่ จึงไม่ได้มองไม่เห็น Chrome หน่วยความจำของ container เพิ่มจาก 294 MiB เป็น 303 MiB แล้วเริ่มแบนในตัวอย่าง นี่ไม่ใช่ผลการทดสอบความทนทานระดับหลายชั่วโมง แบบ concurrent หรือระดับหลายพัน session


