diff --git a/docs/api/advanced/reporters.md b/docs/api/advanced/reporters.md
index 12f4476d5252..30f2f3f8d973 100644
--- a/docs/api/advanced/reporters.md
+++ b/docs/api/advanced/reporters.md
@@ -55,7 +55,7 @@ function onInit(vitest: Vitest): Awaitable<void>
 This method is called when [Vitest](/api/advanced/vitest) was initiated or started, but before the tests were filtered.
 
 ::: info
-Internally this method is called inside [`vitest.start`](/api/advanced/vitest#start), [`vitest.init`](/api/advanced/vitest#init) or [`vitest.mergeReports`](/api/advanced/vitest#mergereports). If you are using programmatic API, make sure to call either one depending on your needs before calling [`vitest.runTestSpecifications`](/api/advanced/vitest#runtestspecifications), for example. Built-in CLI will always run methods in correct order.
+Internally this method is called inside [`vitest.start`](/api/advanced/vitest#start), [`vitest.standalone`](/api/advanced/vitest#standalone) or [`vitest.mergeReports`](/api/advanced/vitest#mergereports). If you are using programmatic API, make sure to call either one depending on your needs before calling [`vitest.runTestSpecifications`](/api/advanced/vitest#runtestspecifications), for example. Built-in CLI will always run methods in correct order.
 :::
 
 Note that you can also get access to `vitest` instance from test cases, suites and test modules via a [`project`](/api/advanced/test-project) property, but it might also be useful to store a reference to `vitest` in this method.
diff --git a/docs/api/advanced/vitest.md b/docs/api/advanced/vitest.md
index 98609be1054b..83c2d23c2732 100644
--- a/docs/api/advanced/vitest.md
+++ b/docs/api/advanced/vitest.md
@@ -233,17 +233,19 @@ function start(filters?: string[]): Promise<TestRunResult>
 Initialize reporters, the coverage provider, and run tests. This method accepts string filters to match the test files - these are the same filters that [CLI supports](/guide/filtering#cli).
 
 ::: warning
-This method should not be called if [`vitest.init()`](#init) is also invoked. Use [`runTestSpecifications`](#runtestspecifications) or [`rerunTestSpecifications`](#reruntestspecifications) instead if you need to run tests after Vitest was initialised.
+This method should not be called if [`vitest.standalone()`](#standalone) is also invoked. Use [`runTestSpecifications`](#runtestspecifications) or [`rerunTestSpecifications`](#reruntestspecifications) instead if you need to run tests after Vitest was initialised.
 :::
 
 This method is called automatically by [`startVitest`](/guide/advanced/tests) if `config.mergeReports` and `config.standalone` are not set.
 
-## init
+## standalone <Version type="experimental">4.1.1</Version> {#standalone}
 
 ```ts
-function init(): Promise<void>
+function standalone(): Promise<void>
 ```
 
+- **Alias**: `init` <Deprecated />
+
 Initialize reporters and the coverage provider. This method doesn't run any tests. If the `--watch` flag is provided, Vitest will still run changed tests even if this method was not called.
 
 Internally, this method is called only if [`--standalone`](/guide/cli#standalone) flag is enabled.
@@ -545,7 +547,7 @@ If there is a test run happening, returns a promise that will resolve when the t
 function createCoverageProvider(): Promise<CoverageProvider | null>
 ```
 
-Creates a coverage provider if `coverage` is enabled in the config. This is done automatically if you are running tests with [`start`](#start) or [`init`](#init) methods.
+Creates a coverage provider if `coverage` is enabled in the config. This is done automatically if you are running tests with [`start`](#start) or [`standalone`](#standalone) methods.
 
 ::: warning
 This method will also clean all previous reports if [`coverage.clean`](/config/coverage#coverage-clean) is not set to `false`.
diff --git a/docs/api/expect.md b/docs/api/expect.md
index 9a928988d940..3ae4f95a7243 100644
--- a/docs/api/expect.md
+++ b/docs/api/expect.md
@@ -1079,7 +1079,7 @@ test('calls mock1 after mock2', () => {
 })
 ```
 
-## toHaveBeenCalledExactlyOnceWit
+## toHaveBeenCalledExactlyOnceWith
 
 - **Type**: `(...args: any[]) => Awaitable<void>`
 
diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md
index 6b82b3926181..45f69a8096f7 100644
--- a/docs/guide/cli-generated.md
+++ b/docs/guide/cli-generated.md
@@ -853,7 +853,7 @@ Use `bundle` to bundle the config with esbuild or `runner` (experimental) to pro
 
 - **CLI:** `--standalone`
 
-Start Vitest without running tests. Tests will be running only on change. This option is ignored when CLI file filters are passed. (default: `false`)
+Start Vitest without running tests. Tests will be running only on change. If browser mode is enabled, the UI will be opened automatically. This option is ignored when CLI file filters are passed. (default: `false`)
 
 ### listTags
 
diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts
index d072704cf327..0ec33f732fd6 100644
--- a/packages/vitest/src/node/cli/cli-api.ts
+++ b/packages/vitest/src/node/cli/cli-api.ts
@@ -93,7 +93,7 @@ export async function startVitest(
 
   ctx.onAfterSetServer(() => {
     if (ctx.config.standalone) {
-      ctx.init()
+      ctx.standalone()
     }
     else {
       ctx.start(cliFilters)
@@ -111,7 +111,7 @@ export async function startVitest(
       await ctx.mergeReports()
     }
     else if (ctx.config.standalone) {
-      await ctx.init()
+      await ctx.standalone()
     }
     else {
       await ctx.start(cliFilters)
diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts
index ddf7641096b2..194ecd8e56b4 100644
--- a/packages/vitest/src/node/cli/cli-config.ts
+++ b/packages/vitest/src/node/cli/cli-config.ts
@@ -807,7 +807,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
   },
   standalone: {
     description:
-      'Start Vitest without running tests. Tests will be running only on change. This option is ignored when CLI file filters are passed. (default: `false`)',
+      'Start Vitest without running tests. Tests will be running only on change. If browser mode is enabled, the UI will be opened automatically. This option is ignored when CLI file filters are passed. (default: `false`)',
   },
   mergeReports: {
     description:
diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts
index c753835c9e0a..269deee75c56 100644
--- a/packages/vitest/src/node/core.ts
+++ b/packages/vitest/src/node/core.ts
@@ -794,11 +794,19 @@ export class Vitest {
     })
   }
 
+  /**
+   * @deprecated use `standalone()` instead
+   */
+  init(): Promise<void> {
+    this.logger.deprecate('`vitest.init()` is deprecated. Use `vitest.standalone()` instead.')
+    return this.standalone()
+  }
+
   /**
    * Initialize reporters and the coverage provider. This method doesn't run any tests.
    * If the `--watch` flag is provided, Vitest will still run changed tests even if this method was not called.
    */
-  async init(): Promise<void> {
+  async standalone(): Promise<void> {
     await this._traces.$('vitest.init', async () => {
       try {
         await this.initCoverageProvider()
@@ -811,6 +819,8 @@ export class Vitest {
       // populate test files cache so watch mode can trigger a file rerun
       await this.globTestSpecifications()
 
+      await Promise.all(this.projects.map(project => project._standalone()))
+
       if (this.config.watch) {
         await this.report('onWatcherStart')
       }
diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts
index c96cb73dc921..d429072866d9 100644
--- a/packages/vitest/src/node/logger.ts
+++ b/packages/vitest/src/node/logger.ts
@@ -258,7 +258,7 @@ export class Logger {
     }
 
     if (this.ctx.config.standalone) {
-      this.log(c.yellow(`\nVitest is running in standalone mode. Edit a test file to rerun tests.`))
+      this.log(c.yellow(`\nVitest is running in standalone mode. Edit a test file to rerun tests.\n`))
     }
     else {
       this.log()
diff --git a/packages/vitest/src/node/pools/browser.ts b/packages/vitest/src/node/pools/browser.ts
index b9048ef712ca..69e6077c60d6 100644
--- a/packages/vitest/src/node/pools/browser.ts
+++ b/packages/vitest/src/node/pools/browser.ts
@@ -41,18 +41,8 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
 
     debug?.('creating pool for project %s', project.name)
 
-    const resolvedUrls = project.browser!.vite.resolvedUrls
-    const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0]
-
-    if (!origin) {
-      throw new Error(
-        `Can't find browser origin URL for project "${project.name}"`,
-      )
-    }
-
     const pool: BrowserPool = new BrowserPool(project, {
       maxWorkers: getThreadsCount(project),
-      origin,
     })
     projectPools.set(project, pool)
     vitest.onCancel(() => {
@@ -198,7 +188,7 @@ class BrowserPool {
   private _promise: DeferPromise<void> | undefined
   private _providedContext: string | undefined
 
-  private readySessions = new Set<string>()
+  private readySessions: Set<string>
 
   private _traces: Traces
   private _otel: {
@@ -210,7 +200,6 @@ class BrowserPool {
     private project: TestProject,
     private options: {
       maxWorkers: number
-      origin: string
     },
   ) {
     this._traces = project.vitest._traces
@@ -219,6 +208,7 @@ class BrowserPool {
       'vitest.project': project.name,
       'vitest.browser.provider': this.project.browser!.provider.name,
     })
+    this.readySessions = project._browserReadySessions
   }
 
   public cancel(): void {
@@ -296,24 +286,10 @@ class BrowserPool {
   }
 
   private async openPage(sessionId: string, options: { parallel: boolean }): Promise<void> {
-    const sessionPromise = this.project.vitest._browserSessions.createSession(
-      sessionId,
-      this.project,
-      this,
-    )
-    const browser = this.project.browser!
-    const url = new URL('/__vitest_test__/', this.options.origin)
-    url.searchParams.set('sessionId', sessionId)
-    const otelCarrier = this._traces.getContextCarrier()
-    if (otelCarrier) {
-      url.searchParams.set('otelCarrier', JSON.stringify(otelCarrier))
-    }
-    const pagePromise = browser.provider.openPage(
-      sessionId,
-      url.toString(),
-      options,
-    )
-    await Promise.all([sessionPromise, pagePromise])
+    await this.project._openBrowserPage(sessionId, {
+      reject: error => this.reject(error),
+      parallel: options.parallel,
+    })
   }
 
   private getOrchestrator(sessionId: string) {
diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts
index 407ac880399e..b6f687db692e 100644
--- a/packages/vitest/src/node/project.ts
+++ b/packages/vitest/src/node/project.ts
@@ -15,6 +15,7 @@ import type {
   TestProjectInlineConfiguration,
   UserConfig,
 } from './types/config'
+import crypto from 'node:crypto'
 import { promises as fs, readFileSync } from 'node:fs'
 import { rm } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
@@ -70,6 +71,7 @@ export class TestProject {
   /** @internal */ _fetcher!: VitestFetchFunction
   /** @internal */ _serializedDefines?: string
   /** @internal */ testFilesList: string[] | null = null
+  /** @internal */ _browserReadySessions = new Set<string>()
 
   private runner!: ModuleRunner
 
@@ -604,6 +606,66 @@ export class TestProject {
     ]
   }
 
+  /** @internal */
+  public async _openBrowserPage(sessionId: string, pool: {
+    reject: (error: Error) => void
+    parallel?: boolean
+  }): Promise<void> {
+    if (!this.browser) {
+      throw new Error(`browser is not initialized`)
+    }
+
+    const resolvedUrls = this.browser.vite.resolvedUrls
+    const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0]
+    if (!origin) {
+      throw new Error(
+        `Can't find browser origin URL for project "${this.name}"`,
+      )
+    }
+
+    const url = new URL('/__vitest_test__/', origin)
+    url.searchParams.set('sessionId', sessionId)
+    const otelCarrier = this.vitest._traces.getContextCarrier()
+    if (otelCarrier) {
+      url.searchParams.set('otelCarrier', JSON.stringify(otelCarrier))
+    }
+    this.vitest._browserSessions.sessionIds.add(sessionId)
+    const sessionPromise = this.vitest._browserSessions.createSession(
+      sessionId,
+      this,
+      pool,
+    )
+    const pagePromise = this.browser.provider.openPage(
+      sessionId,
+      url.toString(),
+      { parallel: pool.parallel ?? false },
+    )
+    await Promise.all([
+      sessionPromise,
+      pagePromise,
+    ])
+  }
+
+  /** @internal */
+  public async _standalone(): Promise<void> {
+    if (!this.isBrowserEnabled()) {
+      return
+    }
+
+    await this._initBrowserProvider()
+    if (!this.browser) {
+      return
+    }
+
+    const sessionId = crypto.randomUUID()
+    await this._openBrowserPage(sessionId, {
+      reject: (error) => {
+        this.vitest.state.catchError(error, 'Browser Error')
+      },
+    })
+    this._browserReadySessions.add(sessionId)
+  }
+
   private _serializeOverriddenConfig(): SerializedConfig {
     // TODO: serialize the config _once_ or when needed
     const config = serializeConfig(this)
