Skip to content

idling? blocks for full browser.timeout when lazy iframes never fire Page.frameStoppedLoading #583

@agrberg

Description

@agrberg

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):

  1. A loading="lazy" iframe inside a closed <details> element, present on initial page load.
  2. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions