Describe the bug
When a page contains <iframe loading="lazy"> elements that Chrome never loads (because they're outside the viewport), Ferrum blocks for the full browser.timeout on every page visit.
idling? in ferrum/page/frames.rb requires every frame registered via Page.frameAttached to reach :stopped_loading:
def idling?
@frames.values.all? { |f| f.state == :stopped_loading }
end
Chrome fires Page.frameAttached for every <iframe> it discovers in the DOM, including lazy ones it has no intention of loading. It only fires Page.frameStoppedLoading when a frame actually completes or fails a load. For a lazy iframe outside the viewport, Chrome parks it indefinitely. No network request, no load event, no frameStoppedLoading. Ferrum sits in go_to's wait loop until browser.timeout expires.
To Reproduce
Two known triggers (also reported in rubycdp/cuprite#303):
- A
loading="lazy" iframe inside a closed <details> element, present on initial page load.
- A
loading="lazy" iframe dynamically inserted outside the viewport via a JS click handler.
FERRUM_DEBUG=true makes the issue visible. It streams all CDP messages with elapsed timestamps. In scenario 1, the output shows Page.frameAttached firing for two YouTube iframes at ~0.68s, Page.frameStoppedLoading firing only for the main frame at ~0.73s, then silence until the 5s timeout.
Expected behavior
go_to should complete promptly. Lazy iframes that Chrome has chosen not to load should not block the idle check.
Desktop
- OS: macOS
- Browser: HeadlessChrome 148.0.0.0
- Version: ferrum 0.17.2, cuprite 0.17
Additional context
Both Chrome flag approaches are ineffective on Chrome 148:
--disable-features=LazyFrameLoading
--disable-blink-features=LazyFrameLoading
A workaround for scenario 1 is forcing loading="eager" on the iframes in test and using Cuprite's url_blacklist to abort the resulting requests. Chrome then fires frameStoppedLoading almost immediately on the aborted request. This isn't available for scenario 2 where the iframe is created by JavaScript at runtime.
The most targeted fix would be in the frame subscription logic: a frame that reaches the idle check without ever having fired Page.frameStartedLoading (Chrome attached it but never began loading it) could be considered idle rather than stuck. This would handle both scenarios without affecting normal frame lifecycle tracking.
Describe the bug
When a page contains
<iframe loading="lazy">elements that Chrome never loads (because they're outside the viewport), Ferrum blocks for the fullbrowser.timeouton every page visit.idling?inferrum/page/frames.rbrequires every frame registered viaPage.frameAttachedto reach:stopped_loading:Chrome fires
Page.frameAttachedfor every<iframe>it discovers in the DOM, including lazy ones it has no intention of loading. It only firesPage.frameStoppedLoadingwhen a frame actually completes or fails a load. For a lazy iframe outside the viewport, Chrome parks it indefinitely. No network request, no load event, noframeStoppedLoading. Ferrum sits ingo_to's wait loop untilbrowser.timeoutexpires.To Reproduce
Two known triggers (also reported in rubycdp/cuprite#303):
loading="lazy"iframe inside a closed<details>element, present on initial page load.loading="lazy"iframe dynamically inserted outside the viewport via a JS click handler.FERRUM_DEBUG=truemakes the issue visible. It streams all CDP messages with elapsed timestamps. In scenario 1, the output showsPage.frameAttachedfiring for two YouTube iframes at ~0.68s,Page.frameStoppedLoadingfiring only for the main frame at ~0.73s, then silence until the 5s timeout.Expected behavior
go_toshould complete promptly. Lazy iframes that Chrome has chosen not to load should not block the idle check.Desktop
Additional context
Both Chrome flag approaches are ineffective on Chrome 148:
--disable-features=LazyFrameLoading--disable-blink-features=LazyFrameLoadingA workaround for scenario 1 is forcing
loading="eager"on the iframes in test and using Cuprite'surl_blacklistto abort the resulting requests. Chrome then firesframeStoppedLoadingalmost immediately on the aborted request. This isn't available for scenario 2 where the iframe is created by JavaScript at runtime.The most targeted fix would be in the frame subscription logic: a frame that reaches the idle check without ever having fired
Page.frameStartedLoading(Chrome attached it but never began loading it) could be considered idle rather than stuck. This would handle both scenarios without affecting normal frame lifecycle tracking.