When Is a Page Ready?

For Obscura, the difficult question after navigation is not whether a response arrived but whether the page has reached a useful observation point. This chapter follows the transitions from initialization through load, network-idle signals, and bounded settling, then connects those transitions to cookies, storage, tabs, and runtime ownership. The practical judgment is simple to state but not to automate: choose a readiness policy that matches the page’s behavior, and keep persistence separate from isolation.

A static document may be useful as soon as it loads. A single-page application may still be constructing its meaningful DOM, fetching data, and committing components after the load event. Conversely, an advertising timer or animation loop may keep a page technically active long after the content a scraper needs is stable. Obscura’s lifecycle is an attempt to distinguish those cases without allowing any of them to run forever.

Load is a sequence of transitions

Obscura models navigation as a lifecycle rather than a binary success flag. A page begins in initialization, commits a document, reaches domcontentloaded, then load; it can later pass through networkidle2 and networkidle0. A caller chooses a waitUntil level, so “navigation completed” means different things under different contracts.

The early transitions describe increasingly complete stages of document setup. Initialization means the page has begun the navigation process. Commit identifies the point at which a new document is in place. domcontentloaded indicates that the parser has finished the document’s initial structure, while load waits for the load-oriented work tracked by the page. These milestones are useful because they are observable and ordered, but none of them can inspect the intentions of an application framework.

An SPA can reach load with only a shell visible. Its application code may then request JSON, wait for a promise, construct components, and insert the records the caller needs. The page is not broken; it has simply not finished the work relevant to the extraction. That is why Obscura also tracks network-idle levels. They provide stronger signals about outstanding requests, but “no active network request at this instant” still does not mean that no future timer will start a request or that the last response has already been turned into the final DOM.

The reverse failure is just as important. A page may run an analytics interval, animation frame, or long-lived connection forever. If readiness required absolute stillness, a caller could wait indefinitely for a page that is behaving normally. Lifecycle stages are therefore thresholds chosen by the caller, not universal truths about application completeness.

stateDiagram-v2 [*] --> init init --> commit commit --> domcontentloaded domcontentloaded --> load load --> networkidle2 networkidle2 --> networkidle0 load --> settling: caller requests settle networkidle2 --> settling: caller requests settle networkidle0 --> ready: chosen waitUntil reached settling --> ready: quiet window observed settling --> settling: timer, fetch, frame, or mutation activity settling --> failed: navigation or settle deadline ready --> [*] failed --> [*]

The diagram shows two related but different decisions. A caller may stop at a lifecycle milestone, or it may ask Obscura to settle the page after navigation. In both cases the page can fail or time out. A successful transition means that the chosen condition was met, not that the application has no possible future work.

Each transition also changes what the caller is entitled to assume. Before commit, the old document may still be the page’s visible state; after commit, the new document owns the DOM and script preparation. After domcontentloaded, the initial tree exists, but application code may still be changing it. After load, the page has passed a stronger resource milestone, but post-load fetches and timers remain possible. A caller that records only a final URL loses these distinctions. A caller that records the chosen milestone and the subsequent settling policy can explain why a result was observed when it was.

Adaptive settling follows useful activity

Obscura’s settle operation exists because a page’s meaningful work often continues after a navigation milestone. It drives the JavaScript event loop so timers, microtasks, in-flight fetches, frame work, and completion callbacks can run. It also gives newly available frame documents and, in render-enabled builds, resources a chance to enter the page state that a later capture will observe.

The naive approaches are both poor. Returning immediately at load produces premature reads. Waiting for the event loop to become absolutely idle can let an analytics interval or animation keep the caller occupied even though the document has stopped changing in every way that matters. Obscura’s adaptive policy instead looks for a quiet period in observable page activity, while giving ordinary external work a bounded grace period. A request or mutation can reset the quiet window; a perpetual timer is not automatically treated as evidence that the page is still becoming more ready.

This is easiest to see with a dashboard. After load, the application starts a request for the current account and schedules a short debounce before rendering the result. An immediate extraction misses the account data. A useful settle policy waits long enough for the request and the commit, notices the resulting DOM activity, and starts the quiet interval after that activity. If the page also schedules a five-minute telemetry interval, the interval should not turn the entire page into an endless readiness promise.

The distinction between external work and observable activity is important. A pending request can deserve a short opportunity to finish after load, but a request that remains open indefinitely should not hold the caller hostage by itself. A connected-document mutation is stronger evidence that the page is still producing visible state. A one-shot timer close to the quiet horizon may also be worth waiting for, while a recurring animation or distant timer is not necessarily a reason to delay capture. This is a policy about evidence of readiness, not a claim that Obscura can understand an application’s business semantics.

The policy is bounded at several levels. Navigation has an end-to-end timeout. Script execution has a configurable deadline. The event-loop pump has an absolute caller budget, and synchronous work is separately protected by a V8 watchdog because an asynchronous timeout cannot interrupt JavaScript that never yields. If a page remains busy until the budget expires, Obscura returns control rather than pretending that the page became complete. That is a meaningful failure mode: the caller can decide whether the partial state is useful, retry with a different policy, or record that the page did not meet its readiness contract.

Strict and fixed waiting remain useful when the caller knows something about the site that an adaptive policy cannot infer. A test fixture may require a fixed interval. A page may expose an application-specific signal that the caller can combine with evaluation. The important distinction is between asking for a deliberate observation window and assuming that “idle” has one universal meaning.

This also changes how errors should be interpreted. A page-local exception or rejected promise does not necessarily stop later tasks; a browser normally reports the error and continues scheduling. An engine-level termination, a navigation deadline, or an unavailable runtime is different. Good automation records the lifecycle and runtime signals alongside the extracted content, so an empty result can be diagnosed rather than silently accepted.

State belongs to different owners

Readiness is only half of correctness. A caller can observe the right moment and still use the wrong state. Obscura separates browser context state from page state, tab state, and connection state because each has a different lifetime.

A browser context carries the network-facing configuration: cookie jar, HTTP client, user-agent identity, proxy, robots cache, storage directory, stealth choice, and private-network policy. Pages created within that context use those settings, but the page itself owns the current document, URL, runtime, lifecycle, viewport, and page-scoped observations such as request and response callbacks. A tab or CDP target adds a caller-facing handle to a page. A connection determines how protocol sessions and runtime work are coordinated. These scopes can overlap, but they are not interchangeable.

Persistence makes the distinction practical. When a storage directory is configured, Obscura can load cookies when the context is created and save them during graceful shutdown; the architecture documentation also describes local-storage persistence. That makes a later run able to resume a session. It does not mean that every page, tab, or process should share those credentials, nor does it mean that a persisted cookie is an isolation boundary.

The context API supports isolated copies with two different intentions. A persistent copy can inherit the current cookies while retaining separate mutable network state. An incognito-like copy starts without those cookies while keeping the browser configuration. The choice is about which state should be copied, not merely whether a new page object receives a new identifier. A service handling unrelated users should treat cookie inheritance as an explicit data decision.

Consider a crawler that logs in once and then opens several pages. Sharing one context may be exactly what is wanted if those pages are part of the same account workflow: they can use the same session cookies and network policy. The same arrangement is unsafe if the pages represent different customers. Creating a separate page does not by itself answer the isolation question, because the context may still carry the cookie jar and client configuration. The safer design makes the context boundary explicit, chooses whether credentials are copied, and persists only the state that the next run is meant to resume.

The separation between file access and private-network access reinforces the same idea. Allowing a local HTML file is a filesystem decision; allowing loopback, RFC1918, or link-local addresses is an egress decision. They may be convenient in development, but enabling one should not silently enable the other. A stateful browser needs policies that remain visible at the boundary where they matter.

Tabs expose runtime lifecycle

Multiple tabs make runtime ownership visible. An MCP session keeps a live browser state with an active tab and can create additional tabs. When the active tab changes, the server suspends inactive page runtimes while retaining their DOM state and resumes the selected tab. That arrangement accommodates the lifecycle requirements of V8 while preserving the agent’s expectation that a tab remains a meaningful object after it is not currently selected.

Suspension is not the same as a fresh navigation. The document state can survive while the live JavaScript runtime is temporarily replaced or removed. When the tab resumes, the engine restores the runtime-facing state needed for that page. This is why an agent can switch tabs without treating every switch as a new page, while also why a reference to an interactive element can become stale after navigation or a framework rerender.

The same distinction appears in CDP, though the exact runtime scope depends on the server and connection path. The repository’s high-level architecture describes pages sharing an isolate in one process, while newer implementation notes describe connection-local isolates and per-connection coordination. MCP explicitly manages active-tab runtimes. The safe operational rule is to rely on the context and interface semantics being used, not to infer security or scheduling isolation from a page label alone.

For an automation caller, state ownership answers questions that readiness alone cannot. Should a second page receive the first page’s cookies? Should a new browser context inherit a logged-in session? Does closing a client connection close the page or only detach the session? Does switching tabs preserve a DOM but pause its JavaScript work? These are different decisions, and confusing them can create either incorrect results or accidental credential exposure.

Choosing an observation point

A sensible readiness policy starts with the page’s behavior. For a mostly static document, waiting for load may be sufficient and avoids unnecessary settling. For an SPA whose useful content arrives through fetches and DOM commits, adaptive settling is a better default because it observes the page’s own activity while retaining a wall-clock bound. For a known test or application-specific delay, a fixed wait can be honest about what the caller is asking. A strict event-loop interval is useful when the goal is to drive timers for a defined period rather than to infer that the page has become quiet.

No policy removes the need to validate the output. Compare the observed DOM or response data with a known page state, inspect network and runtime events when results are suspicious, and keep the chosen timeout visible in the surrounding system. If a page’s work routinely exceeds the budget, increasing the budget may be reasonable, but it changes the availability trade-off and should be paired with a matching client timeout and deployment boundary.

The useful conclusion is not that Obscura has found one perfect definition of readiness. It has made readiness an explicit, bounded choice over a stateful page. Once the caller understands what was loaded, what continued running, which state was retained, and where the runtime lived, the page can be treated as an object of judgment rather than a deceptive snapshot. The next question is how those facts fit together into one compact model before rendering and interface choices add more layers.

References