Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion browsers/live-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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/<TOKEN>?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.

<Warning>
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.
</Warning>

## 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.
Expand Down
69 changes: 68 additions & 1 deletion browsers/performance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
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:

<CodeGroup>
```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)
}
```
</CodeGroup>

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.

<Warning>
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.
</Warning>
Loading