---
slug: crawl4ai-performance-antibot-implementation
status: published
title: Crawl4AI 真正快在哪裡？從 browser reuse 到 anti-bot fallback
excerpt: Crawl4AI 不會讓每個網址自動變快。它的優勢出現在長時間 browser reuse、受控併發、cache、資源阻擋與 anti-bot 失敗升級。這篇直接從 v0.9.2 原始碼拆解這些機制。
category: Development
tags: [Crawl4AI, web-crawling, Playwright, anti-bot, Python, performance]
author: Seer
author_role: Author
read_time: 12 min
cover: "/static/crawl4ai-performance-antibot-implementation-cover.png"
closing_note: "真正穩定的速度，來自每一段成本都有地方被看見。"
published_at: "2026-08-30T00:00:00Z"
updated_at: "2026-08-30T00:00:00Z"
---

Crawl4AI 的首頁把速度和 anti-bot 都放得很前面。只看這些標籤，很容易得到一個印象：把原本的 crawler 換成 Crawl4AI，頁面就會抓得更快，也比較不容易被擋。

問題是，「快」一定要有比較對象。

一個公開的靜態 HTML 頁面，如果用 `httpx` 或 `requests` 就能拿到完整內容，啟動 Chromium 通常只會增加工作。Browser 要建立 process、context、page，還要執行導覽、渲染與 JavaScript。Crawl4AI 仍然要付這些 browser 成本。

比較基準換成動態網站，情況就不同了。最常見的 one-shot Playwright 腳本會替每個網址重新啟動 browser；批次處理直接 `asyncio.gather()`，記憶體不夠才崩；遇到內容還沒出現就加一個固定 `sleep(5)`；收到 HTTP 200，便把 Cloudflare challenge page 當成成功內容。

Crawl4AI 的實作優勢集中在這裡：它把 browser 生命週期、context／session 重用、批次併發、等待條件、資源載入與 anti-bot recovery 做成同一個 runtime。省下來的時間往往不是單次 HTTP latency，而是少做重複初始化、減少無效等待、避免重抓，以及讓批次工作不用在記憶體爆掉後重來。[1][2]

這篇固定在 Crawl4AI v0.9.2、commit `7e801521428ee12509994d39151006f64055ebe3`。以下結論來自官方文件與原始碼靜態查核，沒有執行 Crawl4AI，也沒有做跨工具 benchmark。凡是速度、記憶體或 anti-bot 成功率，都只能說明實作可能帶來的優勢，不能當成已量測結果。

## 第一段時間差：browser 不必每抓一頁就重開

`AsyncWebCrawler` 的生命週期很單純：進入 `async with` 時呼叫 `start()`，離開時才 `close()`。在這段期間內，可以連續呼叫多次 `arun()` 或一次送進 `arun_many()`。[2]

```python
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig

browser_config = BrowserConfig(headless=True)
run_config = CrawlerRunConfig()

async with AsyncWebCrawler(config=browser_config) as crawler:
    first = await crawler.arun(
        "https://example.com/page-1",
        config=run_config,
    )
    second = await crawler.arun(
        "https://example.com/page-2",
        config=run_config,
    )
```

這和「每個 URL 建立一個 `AsyncWebCrawler`」差很多。Crawler strategy 和底層 browser 只啟動一次，後面的頁面沿用同一段 runtime。

再往下看 `BrowserManager`，它會按照 browser context 真的需要哪些設定，計算一個 config signature。Proxy、locale、timezone、geolocation 與 navigator overrides 相同的 crawl，可以共用既有 context；只為 extraction、CSS selector 或 screenshot 改設定，不會無條件建立新 context。[3]

批次抓取時，共用 context 不代表所有 URL 搶同一個 tab。非 managed path 和 isolated-context path 都會從相同設定的 context 建立新 page，讓多個 URL 可以同時導覽。Managed browser 模式則會先找目前沒有被使用的 page，沒有才建立新 page。[3]

這段設計省的是 browser/context 初始化與設定注入成本。它不代表單頁一定比手寫 Playwright 快，但會比「每頁重啟 browser」少掉一整段重複工作。

### `session_id` 連 page 都能保留

如果下一步需要沿用登入狀態、目前 DOM 或 JavaScript state，可以在 `CrawlerRunConfig` 設定 `session_id`。`BrowserManager` 會把對應的 page 和 context 放進 session map；同一個 ID 再次出現時，直接傳回原本的 page/context，而不是建立新的 page。[3][5]

```python
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode

session_config = CrawlerRunConfig(
    session_id="vendor-portal",
    cache_mode=CacheMode.BYPASS,
)

async with AsyncWebCrawler() as crawler:
    login_page = await crawler.arun(
        "https://example.com/login",
        config=session_config,
    )

    account_page = await crawler.arun(
        "https://example.com/account",
        config=session_config,
    )

    await crawler.crawler_strategy.kill_session("vendor-portal")
```

`session_id` 適合多步驟操作，也能搭配 `js_only=True`，讓 SPA 在同一個 page 上繼續點擊或載入下一批內容。它不適合拿同一個 ID 做平行導覽；同一個 page 同時前往兩個 URL，本來就會互相干擾。

如果狀態需要跨 process 保存，`BrowserConfig` 還有 persistent context：

```python
browser_config = BrowserConfig(
    use_persistent_context=True,
    user_data_dir="./profiles/vendor-portal",
)
```

這條路使用 Playwright 的 `launch_persistent_context()`，將 profile 放在 `user_data_dir`。Cookies、local storage 與登入狀態可以留在磁碟上的 profile，下一次 crawler 啟動時接著用。[3][5]

兩種 reuse 解決不同問題：`session_id` 保留目前 crawler runtime 裡的 page；persistent context 保留跨執行的 browser profile。把它們分清楚，比每一頁重新登入更省時間，也能維持比較一致的站點身分。

## 多網址的優勢不在 async，而在 dispatcher

把 `arun()` 改成 async，不會自動得到穩定吞吐。如果一次建立幾十個 browser page，總完成時間可能下降，記憶體、CPU 與目標網站的 rate limit 也會一起升高。

Crawl4AI 的 `arun_many()` 預設使用 `MemoryAdaptiveDispatcher`。它會把 URL 放進 priority queue，在記憶體壓力低時填滿可用 slots；超過 memory threshold 後暫停派發新工作，進入 critical threshold 時則把新任務重新排隊。等待太久的 URL 還會被提高優先度，避免一直卡在隊尾。[2][4]

`CrawlerRunConfig.semaphore_count` 控制同時允許多少工作。v0.9.2 的 config 預設值是 5，而 `arun_many()` 會把它傳給預設 dispatcher 當成 `max_session_permit`。[2][5]

```python
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig

browser_config = BrowserConfig(
    headless=True,
    avoid_ads=True,
)

run_config = CrawlerRunConfig(
    semaphore_count=6,
    mean_delay=0.5,
    max_range=1.0,
    stream=True,
)

async with AsyncWebCrawler(config=browser_config) as crawler:
    async for result in await crawler.arun_many(
        urls,
        config=run_config,
    ):
        print(result.url, result.success)
```

這組設定同時控制三件事。

`semaphore_count` 限制 in-flight crawls。`mean_delay` 和 `max_range` 會進入 `RateLimiter`，為同一 domain 加上隨機間隔。收到 429 或 503 時，delay 會用帶 jitter 的 exponential backoff 增加；成功後再逐步下降。[2][4]

`stream=True` 則在每個 URL 完成後立刻 yield。它不會減少總工作量，但下游可以先處理已完成的頁面，不必等最慢的 URL。改善的是 time-to-first-result，不是單頁 latency。

這裡的核心取捨很清楚：高 concurrency 可能縮短整批 wall time，也可能造成 browser memory pressure，或讓網站更快判定流量異常。Dispatcher 的優勢是把這個取捨變成可控制的 scheduling 問題，並沒有消除它。

### 「Browser recycling」在 v0.9.2 沒有真的重啟 process

v0.8.5 release note 把 `max_pages_before_recycle` 描述成自動重啟 browser。固定到 v0.9.2 原始碼後，實際路徑比較窄。[3][10]

頁數達到門檻時，`BrowserManager` 會增加 `_browser_version`。這個版本號是 context signature 的一部分，因此後續請求會拿到新 context；舊 context 等 refcount 歸零後才關閉。這樣能避免直接殺掉仍在執行的 page，但這條路徑沒有重新啟動 Chromium process。[3]

`memory_saving_mode=True` 另外會加入 aggressive cache discard 與 V8 heap cap。這些設定偏向控制長時間執行的 memory growth，代價可能是 cache 命中與 JavaScript 執行效能下降。兩個開關預設都沒有啟用，也不應直接寫成「一定更快」。[3][5]

## 另一種省時方式：根本不要做不需要的工作

Browser reuse 省掉初始化，cache 與 resource filtering 則直接減少工作量。

### Cache 要自己打開

`CrawlerRunConfig` 在 v0.9.2 的實際預設是 `CacheMode.BYPASS`。如果希望重複 URL 不再進 browser，要明確設定 `CacheMode.ENABLED`。[2][5]

```python
from crawl4ai import CrawlerRunConfig, CacheMode

run_config = CrawlerRunConfig(
    cache_mode=CacheMode.ENABLED,
    check_cache_freshness=True,
    cache_validation_timeout=5.0,
)
```

`arun()` 會先查本機 cache，再決定是否進入 browser crawl。開啟 freshness check 後，validator 優先使用 ETag 與 Last-Modified；server 沒提供可用條件時，還能讀取 `<head>` 並比較 fingerprint。判定內容未變，就直接使用 cached result。[2][11]

這是最直接的時間優勢：一次完整 browser crawl 可以被較小的 validation request 取代。但 freshness 也有 correctness trade-off。頁面正文改了，`<head>` 沒改，fingerprint 可能看不出來；validation 發生錯誤時，程式目前會 fallback 到 cached result。資料若要求強一致，就不能只靠這組預設語意。[2]

### 不需要的資源在 network layer 就擋掉

`BrowserConfig(avoid_ads=True)` 會在 browser context 註冊 route，阻擋 Google Analytics、DoubleClick、Hotjar、Facebook 等一組內建 ad/tracker patterns。`avoid_css=True` 會阻擋 `.css`、`.less`、`.scss`、`.sass`。[3]

這和事後從 HTML 刪除圖片或廣告不同。Request 在 network layer 就被 abort，相關下載與部分處理成本不會發生。

`text_mode=True` 更激進。除了阻擋圖片、字型、影音與部分靜態副檔名，原始碼還把 `--disable-javascript` 放進 browser flags。[3]

這裡要保留 runtime 邊界：本輪沒有啟動 Chromium，無法證明固定版本所使用的 browser 實際接受並執行這個 flag。因此，`text_mode` 可以確定會阻擋多類靜態資源；JavaScript 是否真的被關閉仍需實測。對 React、Vue 等 client-rendered 頁面，至少要先確認正文仍會出現，再把它用成 production profile。

因此，優化順序最好從 `avoid_ads` 開始，再視頁面驗證 `avoid_css`。`text_mode` 應該當成另一種 crawl profile，不能把它視為所有網站通用的加速按鈕。

### 等對條件，比固定 sleep 更省

Crawl4AI 預設 navigation condition 是 `domcontentloaded`，另外把動態頁互動拆成幾個階段：[5][6]

1. `js_code_before_wait` 先觸發載入。
2. `wait_for` 等 CSS selector 或 JavaScript condition。
3. `delay_before_return_html` 留一小段額外時間。
4. `js_code` 在等待完成後執行。
5. 最後才擷取 HTML。

```python
run_config = CrawlerRunConfig(
    js_code_before_wait="document.querySelector('#load-more').click()",
    wait_for="css:.results .item",
    wait_for_timeout=15_000,
    delay_before_return_html=0.2,
)
```

這個設計能取代「每頁固定等五秒」。內容一出現就繼續，慢頁則等到明確 timeout。時間優勢來自條件更精確，不是 Crawl4AI 能預知網站何時完成。

Selector 寫錯，一樣會等滿 timeout；使用 `scan_full_page`、virtual scroll 或 `wait_for_images`，也會主動增加時間。這些功能提升的是內容完整度，不應全部算成效能優化。

## Anti-bot 的優勢，是把四層問題拆開

反爬蟲常被寫成單一能力，實際上至少有四層：browser fingerprint、session identity、request behavior，以及被擋後怎麼判斷和恢復。

Crawl4AI 在每一層都有控制點，但沒有任何一層能保證繞過 WAF 或 CAPTCHA。

### 第一層：減少明顯 automation fingerprint

Chromium 啟動參數包含 `--disable-blink-features=AutomationControlled`。開啟 `enable_stealth=True` 後，Crawl4AI 會套用 `playwright-stealth`，並保留 WebGL，不再使用會讓 headless 特徵更明顯的 GPU 關閉組合。[3][8]

`override_navigator=True`、`simulate_user=True` 或 `magic=True` 會在 page script 執行前注入 navigator override。固定 revision 的 script 會處理 `navigator.webdriver`、`window.navigator.chrome`、plugins、languages、permissions、`document.hidden` 與 visibility state。[3][9]

`simulate_user` 目前做的事情很具體：移動兩次滑鼠，再滾動一小段。它不會建立完整人類行為模型，也不會隨機點擊頁面。Random user agent 則會同步產生 `sec-ch-ua`，避免 UA 和 client hints 直接互相矛盾。[3][6]

這些機制能降低幾個基本 automation signals，處理不了 TLS fingerprint、IP reputation、帳號風險、跨頁行為分析與 CAPTCHA。稱它為 fingerprint reduction 比「繞過反爬蟲」準確。

### 第二層：維持一致的身分

有登入狀態的網站，身份一致通常比每次換 UA 更重要。

Persistent context 可以保留 cookies 與 local storage；`session_id` 可以保留同一個 page/context；`storage_state`、cookies、headers、locale、timezone 與 geolocation 也能在 context 建立時設定。[3][5]

Proxy rotation 還提供 `proxy_session_id`。同一個 proxy session 會持續取得同一個 proxy，直到 TTL 到期、主動 release，或 crawler 關閉。這能避免操作進行到一半突然換 IP。[2][5]

因此，登入型流程不應每個 request 都隨機換 UA 和 proxy。比較合理的做法是為站點建立固定 profile、穩定 UA 與 sticky proxy，再用 session 延續頁面狀態。

### 第三層：控制請求節奏

`arun_many()` 的 per-domain delay 與 429／503 backoff 同時也是 anti-bot 控制。大量 request 在同一秒抵達，即使 fingerprint 很漂亮，行為仍然不像一般使用者。[4]

這裡有一個看似矛盾、實際很重要的取捨：延長 delay 會降低吞吐，卻可能減少 rate limit、retry 與整批失敗。對受保護站點來說，最快的單次設定不一定帶來最短的總完成時間。

### 第四層：判斷「成功回來的是不是垃圾」

Crawl4AI v0.9.2 的 anti-bot detector 不只看 status code。[7]

Tier 1 尋找 Cloudflare、Akamai、PerimeterX、DataDome、Imperva、Sucuri 與 Kasada 等 block-page 結構標記。Tier 2 只在短頁面檢查 `Access Denied`、`Just a moment`、CAPTCHA class 等容易誤判的字樣。Tier 3 再檢查 body、可見文字、內容元素與 script-heavy empty shell，抓出 HTTP 200 但實際沒有內容的回應。[7]

偵測到 blocked 後，`arun()` 會按設定走 retry、proxy chain，最後才呼叫使用者提供的 `fallback_fetch_function`。[2]

```mermaid
flowchart TB
  A[Direct browser crawl] --> B{is_blocked?}
  B -- no --> C[Return CrawlResult]
  B -- yes --> D[Retry / next proxy]
  D --> E{usable result?}
  E -- yes --> C
  E -- no, proxies exhausted --> F[fallback_fetch_function]
  F --> G[Process returned HTML]
  G --> C
```

可以這樣設定一條保守的 escalation path：[5][10]

```python
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
from crawl4ai.async_configs import ProxyConfig

browser_config = BrowserConfig(
    headless=True,
    enable_stealth=True,
    user_agent_mode="random",
)

run_config = CrawlerRunConfig(
    override_navigator=True,
    simulate_user=True,
    proxy_config=[
        ProxyConfig.DIRECT,
        ProxyConfig(server="http://proxy.example:8080"),
    ],
    max_retries=2,
    mean_delay=0.8,
    max_range=1.2,
)

async with AsyncWebCrawler(config=browser_config) as crawler:
    result = await crawler.arun(
        "https://example.com/protected-page",
        config=run_config,
    )

    print(result.crawl_stats)
```

這組 code 沒有附 `fallback_fetch_function`，因為 fallback 是外部能力：可能是自己的 HTTP service，也可能是另外購買的 web unlocker。Crawl4AI 負責呼叫與接回 HTML，不會憑空提供一條能解 challenge 的網路。

Detector 也只是 heuristic。真實文章可能提到 Cloudflare，空白頁也可能是網站自己壞掉；它仍然可能 false-positive 或 false-negative。`crawl_stats` 會記錄 attempts、proxies、blocked reason 與最後由 direct、proxy 還是 fallback 解決，這些資料應該進監控，不能只看 `result.success`。[2][7]

## 怎麼把這些優勢用在真實任務

與其把所有開關一次打開，我會按任務分成三種 profile。

### 公開、以文字為主的批次頁面

維持一個 long-lived `AsyncWebCrawler`，用 `arun_many()` 和 `MemoryAdaptiveDispatcher` 控制併發。先開 `avoid_ads`，確認版面不受影響再測 `avoid_css`。重複抓同一批 URL 時啟用 cache freshness；需要邊抓邊進索引，就開 stream。

這種任務主要受益於 browser/context reuse、batch scheduling、cache 與 resource filtering。若所有頁面都能用直接 HTTP 拿到正文，則應另外保留一組 `httpx`／`requests` 對照，確認 browser 是否值得存在。

### 登入後的多步驟流程

使用 persistent context 保存 profile，以固定 `session_id` 延續目前 page。UA、locale、timezone、cookies 與 proxy 保持一致；同一個 session 依序操作，不拿來做 parallel navigation。

這種任務的重點不是高併發，而是省掉重複登入、降低身分漂移，以及讓 `js_only` 在同一個 SPA state 上繼續執行。

### 容易回傳 challenge page 的目標

先用正常 browser identity 與合理 delay，不要一開始就高速 proxy rotation。再依序加入 stealth、navigator override、block detector、direct→proxy escalation，最後才接 fallback fetcher。

每一層都要保存 `status_code`、blocked reason、attempts、proxy 與最終 HTML。否則只看到「成功率變高」，卻不知道是 stealth 有效、proxy 換對，還是 detector 把某些正常頁面誤判後交給 fallback。

## 它的優勢到底成立在哪裡

Crawl4AI 不會讓 browser crawl 變成免費操作。它真正做得好的地方，是把原本散落在 crawler 周邊的成本收進架構裡：browser 和 context 能重用，多 URL 有 memory-aware dispatcher，等待條件可以取代固定 sleep，cache 和 network routing 能減少工作量，anti-bot 失敗則有 detector、proxy chain 與 fallback path。

因此，它最容易在長時間、多頁、動態、需要狀態延續的任務中取得優勢。只抓一個公開靜態頁、追求最低單頁 latency 時，直接 HTTP client 仍可能更合理。

要把「可能有優勢」變成自己的結論，至少要量三組資料：cold start 與 warm reuse 的單頁時間、固定硬體下的 batch throughput／peak memory，以及固定 target、identity、request rate 下的 block 與 fallback 比例。這些數字必須在自己的網站集合與環境裡跑，不能從 async API 或官方 release 文案推算。

最後，anti-bot 控制不等於存取授權。`check_robots_txt` 在 v0.9.2 預設是 `False`，網站條款、robots、請求負載、個資與資料用途仍要由使用者自己處理。[5]

Crawl4AI 由 UncleCode 開發。這篇依 repository 的 Attribution Requirement 標示來源；授權段落僅整理固定 revision 的檔案內容，不構成法律意見。

## 來源

- [1：Crawl4AI v0.9.2 固定 revision](https://github.com/unclecode/crawl4ai/tree/7e801521428ee12509994d39151006f64055ebe3)
- [2：`AsyncWebCrawler` 執行流程](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/async_webcrawler.py)
- [3：`BrowserManager` 與 context／session 管理](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/browser_manager.py)
- [4：Memory-aware dispatcher 與 rate limiter](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/async_dispatcher.py)
- [5：`BrowserConfig` 與 `CrawlerRunConfig`](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/async_configs.py)
- [6：Browser crawl strategy、等待與 user simulation](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/async_crawler_strategy.py)
- [7：Anti-bot response detector](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/antibot_detector.py)
- [8：Playwright／Patchright browser adapter](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/browser_adapter.py)
- [9：Navigator override script](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/js_snippet/navigator_overrider.js)
- [10：Crawl4AI v0.8.5 release note](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/docs/blog/release-v0.8.5.md)
- [11：Cache freshness validator](https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/cache_validator.py)
