Repository navigation
Performance: geometry/text queries (gBCR, innerText, evaluateAll) are 20–100x slower than Chromium on DOM-heavy pages #1617
Copy link
Copy link
Open
Description
Summary
LayoutPolicy::OnDemand geometry and text queries are functionally correct but ~20–100x slower than Chromium on DOM-heavy pages. On a 6000-element page an agent-style scan (querySelectorAll + getBoundingClientRect per match + innerText) costs ~2.1s on moli vs ~55ms on Chromium.
Reproduction
Test page (6000 nodes: 2000 × div.row > div.c + button + input, ~250KB HTML):
const { chromium } = require('playwright-core');
const b = await chromium.connectOverCDP('http://127.0.0.1:9222'); // MOLI_LAYOUT=true moli serve
const page = b.contexts()[0].pages()[0];
await page.goto('http://127.0.0.1:18777/heavy.html', { waitUntil: 'domcontentloaded' });
// per-op timings measured via performance.now() inside evaluateMeasured (moli 1.1.17, MOLI_LAYOUT=true, macOS arm64 vs Chromium via playwright):
| operation | moli | chromium | factor |
|---|---|---|---|
element.getBoundingClientRect() ×4000 |
1541ms (~0.39ms/call) | ~20ms | ~75x |
document.body.innerText (6k nodes) |
503ms first call, 57ms repeat | ~1ms | ~500x / 57x |
locator.evaluateAll ×4000 elements |
229ms (handle materialization) | ~25ms | ~9x |
innerText per element ×160 |
~14ms | ~1ms | ~14x |
| goto + first paint, 6000-node DOM | ~2.4s | ~300ms | ~8x |
| full observe scan (text+controls+rects) | 2087ms | ~55ms | ~38x |
Source reading (1.1.17 / main @ 8fac63e)
getBoundingClientRect→answer_layout_for_document→with_latest_layout_tree_for_documentreuses the frozen published tree (good — no re-layout per call). The ~0.39ms/call is per-elementclient_rects_for_sourcelookup + quad computation. Per-element memoization exists only forLayoutPolicy::Mock(cached_mock_client_rectincontext_host/activity.rs); the real-layout path has no equivalent.innerTextruns a rendered-text pass per call (moli-renderer-v8/native_bridge/element/content.rs); the second call drops to 57ms, suggesting partial caching but still 50x+ over Chromium.evaluateAllmaterializes one remote object handle per match — the marshaling dominates before user JS runs.
Proposals (by leverage)
- Per-generation rect memoization on the real-layout path — mirror
cached_mock_client_rectkeyed by(DomHandle, visual_state_generation): repeatgBCR/offset*/clientRectsreads on unchanged elements become ~free. Helps every geometry loop, not just scans. - Batch geometry at the protocol level — a single call that resolves N element handles → N rect sets in one frozen-tree walk (or document that
Runtime.callFunctionOnarg arrays should share one handle-resolution pass). innerTextmemoization per (document, generation) — same generation-keyed pattern; the second-call 57ms floor suggests partial caching already exists but per-element subtree text isn't memoized.- Optionally document the trade-off in
LayoutPolicy: OnDemand keeps a snapshot but per-query cost is O(tree-walk); for agent workloads (thousands of reads/snapshot) a generation-keyed memo table is the missing piece.
Context / mitigations already possible client-side
Callers can recover ~8–20x without engine changes:
- Do selector-match + scan +
getBoundingClientRectin oneevaluateinstead ofevaluateAllhandle materialization (2087ms → ~250ms observed). - Cap elements before any geometry read (160 rects ≈ 64ms instead of 1541ms).
- Replace
innerTextwith aTreeWalkertext collector + block-boundary newlines for agent evidence (503ms → ~1ms).
Happy to provide the repro files or test a patch.
Metadata
Metadata
Assignees
Labels
No labels
Activity