From 840ca1f6d7ab39ba3992e2d0684cbef225534ea7 Mon Sep 17 00:00:00 2001 From: robertjamesprior <83608739+robertjamesprior@users.noreply.github.com> Date: Mon, 14 Sep 2026 11:39:30 +0000 Subject: [PATCH] Document page load wait strategy and live view URL handling Performance only covered browser creation latency. Add a page load section with the measured comparison: domcontentloaded plus a selector against load, and the fact that networkidle may never resolve on a long-dwell single-page app. Live view documented readOnly as making the view non-interactive, with nothing about the URL granting control of the browser or readOnly not being enforced server-side. Add a section on handling the URL as a credential, and qualify the readOnly bullet to point at it. Co-Authored-By: Claude Opus 5 --- browsers/live-view.mdx | 26 ++++++++++++++- browsers/performance.mdx | 69 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/browsers/live-view.mdx b/browsers/live-view.mdx index 7cfefa9a..b2253026 100644 --- a/browsers/live-view.mdx +++ b/browsers/live-view.mdx @@ -53,13 +53,37 @@ func main() { ## Query parameters The `browser_live_view_url` supports additional query parameters to customize the live view: -- `readOnly` (bool): when set to `true`, the view will be non-interactive. +- `readOnly` (bool): when set to `true`, the view is non-interactive. This is a display option, not a + security control — see [Treat the URL as a credential](#treat-the-url-as-a-credential). Example: ``` https://api.onkernel.com/browser/live/?readOnly=true ``` +## Treat the URL as a credential + +A live view URL grants control of that browser session. Anyone who holds it can drive the browser, so +handle it the way you'd handle an API key rather than a link. + +Two properties to design around: + +- **`readOnly` is a display option, not a security boundary.** It makes the embedded view + non-interactive for the person looking at it. Don't rely on it to constrain what a recipient can do + with the URL. +- **Deleting the browser is how you revoke access.** The URL stays valid while the browser exists, + independently of whether anyone is watching — see [URL lifetime](#url-lifetime) below. + +Embedding the live view in your own application is the intended use, and it's how most human-in-the-loop +flows work. What matters is that your application decides who reaches it: serve the URL from your +backend behind your own authorization rather than passing it to an end user or sharing it in a link. + + +If your end users complete steps themselves — entering a password, clearing MFA, approving a prompt — +gate the live view behind your own authentication and scope each session to one user. A URL that leaks +exposes that browser's session, including anything it is signed in to. + + ## Embedding in an iframe The live view URL can be embedded in an iframe to integrate the browser view into your own application or dashboard. diff --git a/browsers/performance.mdx b/browsers/performance.mdx index bc6583e0..0f36945d 100644 --- a/browsers/performance.mdx +++ b/browsers/performance.mdx @@ -30,4 +30,71 @@ Certain browser configurations trigger Chromium to restart, which can take sever 4. Browser pool refill rate -Browser pools fill at a [specified rate](https://www.kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute). Read about browser pool lifecycle best practices [here](/browsers/pools#how-browser-pools-work). \ No newline at end of file +Browser pools fill at a [specified rate](https://www.kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute). Read about browser pool lifecycle best practices [here](/browsers/pools#how-browser-pools-work). +### Page load latency + +Browser creation is one number; how long a page takes to become *actionable* is another, and the wait +condition you pick dominates it. Measured against a production workload: + +| Page type | `domcontentloaded` + selector | `load` | +|-----------|-------------------------------|--------| +| Homepage | 2.6s | 6.6s | +| Product page | 5.4s | 8.6s | + +`networkidle` did not resolve within an additional 10s budget on either page type. + +Wait on `domcontentloaded` plus the exact selector for the step you're about to take: + + +```typescript Typescript/Javascript +const response = await kernel.browsers.playwright.execute( + kernelBrowser.session_id, + { + code: ` + await page.goto('https://example.com', { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('#results'); + return await page.title(); + ` + } +); +``` + +```python Python +response = kernel.browsers.playwright.execute( + id=kernel_browser.session_id, + code=""" + await page.goto('https://example.com', { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('#results'); + return await page.title(); + """ +) +``` + +```go Go +response, err := client.Browsers.Playwright.Execute(ctx, sessionID, kernel.BrowserPlaywrightExecuteParams{ + Code: ` + await page.goto('https://example.com', { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('#results'); + return await page.title(); + `, +}) +if err != nil { + panic(err) +} +``` + + +Why the other two cost you: + +- **`load`** waits for subresources you don't need. Third-party advertising and analytics tags routinely + keep loading for seconds after the content you care about is present, and `load` makes you wait for + all of it. +- **`networkidle`** can never resolve on a long-dwell single-page app, where background activity + continues for as long as the page is open. + + +The recommendation is not "always use `domcontentloaded`". It's `domcontentloaded` **plus the exact +selector for your next step** — a content-heavy page can reach an app-ready state well after +`domContentLoaded` fires, and a bare `domcontentloaded` wait can hand you a page your selector isn't +on yet. +