075

Crawl4AI 真正快在哪裡?從 browser reuse 到 anti-bot fallback

Crawl4AI 真正快在哪裡?從 browser reuse 到 anti-bot fallback 封面圖

Crawl4AI 不會讓每個網址自動變快。它的優勢出現在長時間 browser reuse、受控併發、cache、資源阻擋與 anti-bot 失敗升級。這篇直接從 v0.9.2 原始碼拆解這些機制。

Seer

2026-08-30

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

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

一個公開的靜態 HTML 頁面,如果用 httpxrequests 就能拿到完整內容,啟動 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]

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_idBrowserManager 會把對應的 page 和 context 放進 session map;同一個 ID 再次出現時,直接傳回原本的 page/context,而不是建立新的 page。[3][5]

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:

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]

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_delaymax_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]

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_csstext_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。
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=Truesimulate_user=Truemagic=True 會在 page script 執行前注入 navigator override。固定 revision 的 script 會處理 navigator.webdriverwindow.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 DeniedJust a moment、CAPTCHA class 等容易誤判的字樣。Tier 3 再檢查 body、可見文字、內容元素與 script-heavy empty shell,抓出 HTTP 200 但實際沒有內容的回應。[7]

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

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]

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 拿到正文,則應另外保留一組 httpxrequests 對照,確認 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 的檔案內容,不構成法律意見。

來源

真正穩定的速度,來自每一段成本都有地方被看見。

Visits

--

Waiting for Cloudflare metrics.