Skip to content

perf(browser): prewarm the browser while the Vite server starts - #10727

Merged
sheremet-va merged 4 commits into
mainfrom
perf/browser-prewarm
Jul 23, 2026
Merged

sheremet-va merged 4 commits into
mainfrom
perf/browser-prewarm

Conversation

@sheremet-va

Copy link
Copy Markdown
Member

Starts preparing the browser while the Vite server is still being created, so the launch latency overlaps server startup.

Browser providers gain an optional prewarm({ config, vitest }) hook, invoked in createClusterServer right before the browser Vite server is created (the TestProject doesn't exist yet at that point, so the hook receives the resolved config). The playwright provider uses it to start import('playwright') and the browser launch concurrently with server startup.

The speculative launch is always safe: the launch options are resolved by the same code as the real launch and compared at adoption time — on any mismatch the prewarmed browser is discarded and the normal launch path runs. A prewarmed browser that is never adopted is closed with the provider. Prewarming is skipped for connectOptions, persistentContext, and inspector flows. Every browser name resolved from browser.name and browser.instances is warmed.

Browser providers gain an optional prewarm({ config, vitest }) hook,
invoked in createClusterServer right before the browser Vite server is
created. The playwright provider uses it to start import('playwright')
and the browser launch concurrently with server startup; the launch
options are resolved by the same code as the real launch and compared at
adoption time, so on any mismatch the speculative browser is discarded
and the normal path runs. Skipped for connectOptions, persistentContext
and inspector flows. Every browser resolved from browser.name and
browser.instances is warmed.
@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for vitest-dev ready!

Name Link
🔨 Latest commit 4cf5d06
🔍 Latest deploy log https://app.netlify.com/projects/vitest-dev/deploys/6a60f1aff67fbf0008455df3
😎 Deploy Preview https://deploy-preview-10727--vitest-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment thread packages/browser-playwright/src/playwright.ts Outdated
The prewarm hook now receives the browser.instances entries that survive
the --project filter (shared with the expansion via
filterProjectBrowserInstances) instead of deriving browser names from the
unfiltered config. The playwright provider also resolves launch options
with the instance-level headless override and closes never-adopted warm
browsers when Vitest closes, so a project that never initializes its
provider cannot leak a browser in watch mode.
if (provider?.prewarm) {
// instances filtered out by `--project` never become projects, so no
// provider would ever adopt (or close) a browser prepared for them
const instances = filterProjectBrowserInstances(vitest.config.project, config)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably nit. Instead of extracting and reusing filterProjectBrowserInstances, would it also possible to stash already available filtered result somewhere and grab it back here? I think config resolution (and project filtering) is already done till reaching here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this should be simplified

Comment on lines +157 to +162
// `headless` is the only launch-relevant option an instance can override;
// this mirrors how the instance's project config is cloned from the parent
const browserConfig = instance.headless == null
? project.config.browser
: { ...project.config.browser, headless: instance.headless }
const launchOptions = resolveLaunchOptions(browserConfig, project.vitest.config.inspector, options, browserName)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks similar point and I assume ground truth config resolution is available at the point of createClusterServer doing prewarm instead of duplicating instance inheritance behavior.

Slop alert 🤖 but if I nudge agent, it said something like this:

gpt says

A lower-repetition design would carry the already cloned instance configs from expansion into server creation.

1. Add Handoff Metadata

At packages/vitest/src/node/types/config.ts:1270:

export interface ResolvedProjectEntry {
  viteConfig: ResolvedViteConfig
  projectConfig: ResolvedConfig
  hidden?: boolean

  /** Resolved browser-instance configs owned by this hidden parent. */
  browserProjects?: ResolvedConfig[]
}

2. Capture Resolved Configs

At packages/vitest/src/node/projects/resolveProjects.ts:458, filtering already determines the selected instances.

Before adding the hidden parent at line 468:

const browserProjects: ResolvedConfig[] = []

result.push({
  ...entry,
  hidden: true,
  browserProjects,
})

Inside the instance loop, the authoritative config is created at lines 553-554:

const clonedConfig
  = cloneProjectConfigForBrowserInstance(projectConfig, instance)
clonedConfig.name = name

browserProjects.push(clonedConfig)

result.push({
  viteConfig,
  projectConfig: clonedConfig,
})

The same resolved config object is therefore used both for project creation and prewarming.

3. Pass Them Into Server Creation

At resolveProjects.ts:930:

const {
  viteConfig,
  projectConfig,
  hidden,
  browserProjects,
} = entry

At line 958:

const { server, parent } = await createClusterServer(
  vitest,
  viteConfig,
  projectConfig,
  browserProjects,
)

4. Prewarm Effective Providers

At packages/vitest/src/node/plugins/browserLoader.ts:111:

export async function createClusterServer(
  vitest: Vitest,
  viteConfig: ResolvedViteConfig,
  config: ResolvedConfig,
  browserProjects: ResolvedConfig[] = [],
) {

Each resolved project contains its effective provider at project.browser.provider. Grouping by provider also fixes the per-instance-provider issue:

const projectsByProvider = Map.groupBy(
  browserProjects,
  project => project.browser.provider,
)

for (const [provider, projects] of projectsByProvider) {
  provider?.prewarm?.({ vitest, projects })
}

5. Consume Resolved Browser Config

Change the prewarm context at packages/vitest/src/node/types/browser.ts:30:

prewarm?: (ctx: {
  vitest: Vitest
  projects: ResolvedConfig[]
}) => void

Then Playwright at packages/browser-playwright/src/playwright.ts:100 becomes:

prewarm(ctx) {
  for (const project of ctx.projects) {
    prewarmBrowser(ctx.vitest, options, project.browser)
  }
}

project.browser already contains:

  • Effective browser name from resolveProjects.ts:707
  • Effective headless from line 705
  • Effective provider from line 706
  • Inherited trace, UI, and parent browser settings

This removes the second filtering call and the manual reconstruction of browserConfig inside Playwright.

@hi-ogawa hi-ogawa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@sheremet-va
sheremet-va merged commit c17677a into main Jul 23, 2026
26 of 27 checks passed
@sheremet-va
sheremet-va deleted the perf/browser-prewarm branch July 23, 2026 08:57
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Sponsor
SponsoredKunjungi sekarang
Promo