Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
39162c6
feat: add `printImportBreakdown.limit` and truncate importDurations c…
hi-ogawa Jan 7, 2026
4a4eac1
test: e2e
hi-ogawa Jan 7, 2026
f318454
Merge branch 'main' into 01-07-feat_add_printimportbreakdown.limit_an…
hi-ogawa Jan 14, 2026
d27af83
fix: limit default 10 and no "show more"
hi-ogawa Jan 19, 2026
d5dd61d
Merge branch 'main' into 01-07-feat_add_printimportbreakdown.limit_an…
hi-ogawa Jan 19, 2026
c23e7ba
docs: update printImportBreakdown documentation with limit option
hi-ogawa Jan 19, 2026
ddb23b6
docs: no mention of limit = 0
hi-ogawa Jan 19, 2026
3300b35
Merge branch 'main' into 01-07-feat_add_printimportbreakdown.limit_an…
hi-ogawa Jan 19, 2026
e0bfad8
refactor: rename printImportBreakdown to importDurations with clearer…
hi-ogawa Jan 20, 2026
3c70c72
Merge branch 'main' into 01-07-feat_add_printimportbreakdown.limit_an…
hi-ogawa Jan 20, 2026
203f945
fix: restore HTML reporter fixture assets
hi-ogawa Jan 20, 2026
49740a6
fix: decouple UI breakdown visibility from print option
hi-ogawa Jan 20, 2026
d60171b
docs: clarify limit controls both collection and display
hi-ogawa Jan 20, 2026
c6e4aa7
refactor: reorder print before limit in importDurations options
hi-ogawa Jan 20, 2026
809980e
docs: tweak
hi-ogawa Jan 20, 2026
10b1e3f
chore: lint
hi-ogawa Jan 20, 2026
bede611
Merge branch 'main' into 01-07-feat_add_printimportbreakdown.limit_an…
hi-ogawa Jan 21, 2026
7b2c658
docs: Update docs/config/experimental.md
hi-ogawa Jan 21, 2026
aef49cb
Merge branch 'main' into 01-07-feat_add_printimportbreakdown.limit_an…
hi-ogawa Jan 23, 2026
c078665
fix: default importDurations.limit to 0, infer 10 when print or ui en…
hi-ogawa Jan 23, 2026
f26c0b6
fix: default importDurations.limit to 0 (or 10 if print/UI enabled)
hi-ogawa Jan 23, 2026
9c58b06
docs: update importDurations.limit default in CLI docs
hi-ogawa Jan 23, 2026
85423a4
fix: add importDurations.limit to reported-tasks test
hi-ogawa Jan 24, 2026
9372508
Merge branch 'main' into 01-07-feat_add_printimportbreakdown.limit_an…
sheremet-va Jan 24, 2026
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
38 changes: 34 additions & 4 deletions docs/config/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,32 @@ export default defineConfig({
It's important that Node can process `sdkPath` content because it is not transformed by Vitest. See [the guide](/guide/open-telemetry) on how to work with OpenTelemetry inside of Vitest.
:::

## experimental.printImportBreakdown <Version type="experimental">4.0.15</Version> {#experimental-printimportbreakdown}
## experimental.importDurations <Version type="experimental">4.1.0</Version> {#experimental-importdurations}

::: tip FEEDBACK
Please leave feedback regarding this feature in a [GitHub Discussion](https://github.com/vitest-dev/vitest/discussions/9224).
:::

- **Type:** `boolean`
- **Default:** `false`
- **Type:**

```ts
interface ImportDurationsOptions {
/**
* Print import breakdown to CLI terminal after tests finish.
*/
print?: boolean
/**
* Maximum number of imports to collect and display.
*/
limit?: number
}
```

Show import duration breakdown after tests have finished running. This option only works with [`default`](/guide/reporters#default), [`verbose`](/guide/reporters#verbose), or [`tree`](/guide/reporters#tree) reporters.
- **Default:** `{ print: false, limit: 0 }` (`limit` is 10 if `print` or UI is enabled)

Configure import duration collection and display.

The `print` option controls CLI terminal output. The `limit` option controls how many imports to collect and display. [Vitest UI](/guide/ui#import-breakdown) can always toggle the breakdown display regardless of the `print` setting.

- Self: the time it took to import the module, excluding static imports;
- Total: the time it took to import the module, including static imports. Note that this does not include `transform` time of the current module.
Expand All @@ -194,6 +210,20 @@ Show import duration breakdown after tests have finished running. This option on

Note that if the file path is too long, Vitest will truncate it at the start until it fits 45 character limit.

### experimental.importDurations.print {#experimental-importdurationsprint}

- **Type:** `boolean`
- **Default:** `false`

Print import breakdown to CLI terminal after tests finish. This only works with [`default`](/guide/reporters#default), [`verbose`](/guide/reporters#verbose), or [`tree`](/guide/reporters#tree) reporters.

### experimental.importDurations.limit {#experimental-importdurationslimit}

- **Type:** `number`
- **Default:** `0` (or `10` if `print` or UI is enabled)

Maximum number of imports to collect and display in CLI output, [Vitest UI](/guide/ui#import-breakdown), and third-party reporters.

::: info
[Vitest UI](/guide/ui#import-breakdown) shows a breakdown of imports automatically if at least one file took longer than 500 milliseconds to load. You can manually set this option to `false` to disable this.
:::
Expand Down
15 changes: 11 additions & 4 deletions docs/guide/cli-generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -838,12 +838,19 @@ Should Vitest throw an error if test has a tag that is not defined in the config

Enable caching of modules on the file system between reruns.

### experimental.printImportBreakdown
### experimental.importDurations.print

- **CLI:** `--experimental.printImportBreakdown`
- **Config:** [experimental.printImportBreakdown](/config/experimental#experimental-printimportbreakdown)
- **CLI:** `--experimental.importDurations.print`
- **Config:** [experimental.importDurations.print](/config/experimental#experimental-importdurations-print)

Print import breakdown after the summary. If the reporter doesn't support summary, this will have no effect. Note that UI's "Module Graph" tab always has an import breakdown.
Print import breakdown to CLI terminal after tests finish (default: false).

### experimental.importDurations.limit

- **CLI:** `--experimental.importDurations.limit <number>`
- **Config:** [experimental.importDurations.limit](/config/experimental#experimental-importdurations-limit)

Maximum number of imports to collect and display (default: 0, or 10 if print or UI is enabled).

### experimental.viteModuleRunner

Expand Down
4 changes: 2 additions & 2 deletions docs/guide/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ If you are developing a custom integration on top of Vitest, you can use [`vites
Please, leave feedback regarding this feature in a [GitHub Discussion](https://github.com/vitest-dev/vitest/discussions/9224).
:::

The Module Graph tab also provides an Import Breakdown with a list of modules that take the longest time to load (top 10 by default, but you can press "Show more" to load 10 more), sorted by Total Time.
The Module Graph tab also provides an Import Breakdown with a list of modules that take the longest time to load (top 10 by default), sorted by Total Time.

<img alt="Import breakdown with a list of top 10 modules that take the longest time to load" img-light src="/ui/light-import-breakdown.png">
<img alt="Import breakdown with a list of top 10 modules that take the longest time to load" img-dark src="/ui/dark-import-breakdown.png">
Expand All @@ -144,4 +144,4 @@ The breakdown shows a list of modules with self time, total time, and a percenta

The "Show Import Breakdown" icon will have a red color if there is at least one file that took longer than 500 milliseconds to load, and it will be orange if there is at least one file that took longer than 100 milliseconds.

By default, Vitest shows the breakdown automatically if there is at least one module that took longer than 500 milliseconds to load. You can control the behaviour by setting the [`experimental.printImportBreakdown`](/config/experimental#experimental-printimportbreakdown) option.
You can use [`experimental.importDurations.limit`](/config/experimental#experimental-importdurationslimit) to control the number of imports displayed.
22 changes: 5 additions & 17 deletions packages/ui/client/components/ModuleGraphImportBreakdown.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ModuleType } from '~/composables/module-graph'
import { relative } from 'pathe'
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { config } from '~/composables/client'
import { currentModule } from '~/composables/navigation'
import { formatTime, getDurationClass, getExternalModuleName } from '~/utils/task'
Expand All @@ -22,9 +22,7 @@ const emit = defineEmits<{
select: [moduleId: string, type: ModuleType]
}>()

const maxAmount = ref(10)

const sortedImports = computed(() => {
const imports = computed(() => {
const file = currentModule.value
const importDurations = file?.importDurations
if (!importDurations) {
Expand All @@ -50,12 +48,9 @@ const sortedImports = computed(() => {
external: duration.external,
})
}
const sortedImports = allImports.sort((a, b) => b.totalTime - a.totalTime)
return sortedImports
return allImports.sort((a, b) => b.totalTime - a.totalTime)
})

const imports = computed(() => sortedImports.value.slice(0, maxAmount.value))

function ellipsisFile(moduleId: string) {
if (moduleId.length <= 45) {
return moduleId
Expand All @@ -67,7 +62,7 @@ function ellipsisFile(moduleId: string) {
<template>
<div class="overflow-auto max-h-120">
<h1 my-2 mx-4>
Import Duration Breakdown <span op-70>(ordered by Total Time) (Top {{ Math.min(maxAmount, imports.length) }})</span>
Import Duration Breakdown <span op-70>(ordered by Total Time)</span>
</h1>
<table my-2 mx-4 text-sm font-light op-90>
<thead>
Expand Down Expand Up @@ -102,17 +97,10 @@ function ellipsisFile(moduleId: string) {
{{ row.formattedTotalTime }}
</td>
<td pr-2 :class="row.totalTimeClass">
{{ Math.round((row.totalTime / sortedImports[0].totalTime) * 100) }}%
{{ Math.round((row.totalTime / imports[0].totalTime) * 100) }}%
</td>
</tr>
</tbody>
</table>
<button
v-if="maxAmount < sortedImports.length"
class="flex w-full justify-center h-8 text-sm z-10 relative font-light"
@click="maxAmount += 5"
>
Show more
</button>
</div>
</template>
4 changes: 2 additions & 2 deletions packages/ui/client/components/views/ViewModuleGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
PositionInitializers,
} from 'd3-graph-controller'
import { computed, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue'
import { config, isReport } from '~/composables/client'
import { isReport } from '~/composables/client'
import { currentModule } from '~/composables/navigation'
import IconButton from '../IconButton.vue'
import Modal from '../Modal.vue'
Expand Down Expand Up @@ -56,7 +56,7 @@ const breakdownIconClass = computed(() => {
}
return textClass
})
const breakdownShow = ref(config.value?.experimental?.printImportBreakdown ?? breakdownIconClass.value === 'text-red')
const breakdownShow = ref(breakdownIconClass.value === 'text-red')

onMounted(() => {
filteredGraph.value = filterGraphByLevels(graph.value, null, 2)
Expand Down
20 changes: 18 additions & 2 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,8 +810,24 @@ export const cliOptionsConfig: VitestCLIOptions = {
},
fsModuleCachePath: null,
openTelemetry: null,
printImportBreakdown: {
description: 'Print import breakdown after the summary. If the reporter doesn\'t support summary, this will have no effect. Note that UI\'s "Module Graph" tab always has an import breakdown.',
importDurations: {
description: 'Configure import duration collection and CLI display. Note that UI\'s "Module Graph" tab can always show import breakdown regardless of the `print` setting.',
argument: '',
transform(value) {
if (typeof value === 'boolean') {
return { print: value }
}
return value
},
subcommands: {
print: {
description: 'Print import breakdown to CLI terminal after tests finish (default: false).',
},
limit: {
description: 'Maximum number of imports to collect and display (default: 0, or 10 if print or UI is enabled).',
argument: '<number>',
},
},
},
viteModuleRunner: {
description: 'Control whether Vitest uses Vite\'s module runner to run the code or fallback to the native `import`. (default: `true`)',
Expand Down
8 changes: 7 additions & 1 deletion packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ export function resolveConfig(
resolved.testTimeout ??= resolved.browser.enabled ? 15_000 : 5_000
resolved.hookTimeout ??= resolved.browser.enabled ? 30_000 : 10_000

resolved.experimental ??= {}
resolved.experimental ??= {} as any
if (resolved.experimental.openTelemetry?.sdkPath) {
const sdkPath = resolve(
resolved.root,
Expand All @@ -867,6 +867,12 @@ export function resolveConfig(
resolved.experimental.fsModuleCachePath,
)
}
resolved.experimental.importDurations ??= {} as any
resolved.experimental.importDurations.print ??= false
if (resolved.experimental.importDurations.limit == null) {
const shouldCollect = resolved.experimental.importDurations.print || resolved.ui
resolved.experimental.importDurations.limit = shouldCollect ? 10 : 0
}

return resolved
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/config/serializeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function serializeConfig(project: TestProject): SerializedConfig {
: project._serializedDefines || '',
experimental: {
fsModuleCache: config.experimental.fsModuleCache ?? false,
printImportBreakdown: config.experimental.printImportBreakdown,
importDurations: config.experimental.importDurations,
viteModuleRunner: config.experimental.viteModuleRunner ?? true,
nodeLoader: config.experimental.nodeLoader ?? true,
openTelemetry: config.experimental.openTelemetry,
Expand Down
7 changes: 4 additions & 3 deletions packages/vitest/src/node/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ export abstract class BaseReporter implements Reporter {
}
}

if (this.ctx.config.experimental.printImportBreakdown) {
if (this.ctx.config.experimental.importDurations.print) {
this.printImportsBreakdown()
}

Expand Down Expand Up @@ -649,14 +649,15 @@ export abstract class BaseReporter implements Reporter {

const sortedImports = allImports.sort((a, b) => b.totalTime - a.totalTime)
const maxTotalTime = sortedImports[0].totalTime
const topImports = sortedImports.slice(0, 10)
const limit = this.ctx.config.experimental.importDurations.limit
const topImports = sortedImports.slice(0, limit)

const totalSelfTime = allImports.reduce((sum, imp) => sum + imp.selfTime, 0)
const totalTotalTime = allImports.reduce((sum, imp) => sum + imp.totalTime, 0)
const slowestImport = sortedImports[0]

this.log()
this.log(c.bold('Import Duration Breakdown') + c.dim(' (ordered by Total Time) (Top 10)'))
this.log(c.bold('Import Duration Breakdown') + c.dim(` (ordered by Total Time) (Top ${limit})`))

// if there are multiple files, it's highly possible that some of them will import the same large file
// we group them to show the distinction between those files more easily
Expand Down
26 changes: 23 additions & 3 deletions packages/vitest/src/node/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,11 +856,24 @@ export interface InlineConfig {
browserSdkPath?: string
}
/**
* Show imports (top 10) that take a long time.
* Configure import duration collection and display.
*
* Enabling this will also show a breakdown by default in UI, but you can always press a button to toggle it.
* The `limit` option controls how many imports to collect and display.
* The `print` option controls CLI terminal output.
* UI can always toggle the breakdown display regardless of `print` setting.
*/
printImportBreakdown?: boolean
importDurations?: {
/**
* Print import breakdown to CLI terminal after tests finish.
* @default false
*/
print?: boolean
/**
* Maximum number of imports to collect and display.
* @default 0 (or 10 if `print` or UI is enabled)
*/
limit?: number
}

/**
* Controls whether Vitest uses Vite's module runner to run the code or fallback to the native `import`.
Expand Down Expand Up @@ -1142,6 +1155,13 @@ export interface ResolvedConfig
vmMemoryLimit?: UserConfig['vmMemoryLimit']
dumpDir?: string
tagsFilter?: string[]

experimental: Omit<Required<UserConfig>['experimental'], 'importDurations'> & {
importDurations: {
print: boolean
limit: number
}
}
}

type NonProjectOptions
Expand Down
5 changes: 4 additions & 1 deletion packages/vitest/src/runtime/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ export interface SerializedConfig {
serializedDefines: string
experimental: {
fsModuleCache: boolean
printImportBreakdown: boolean | undefined
importDurations: {
print: boolean
limit: number
}
viteModuleRunner: boolean
nodeLoader: boolean
openTelemetry: {
Expand Down
17 changes: 14 additions & 3 deletions packages/vitest/src/runtime/runners/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,21 @@ export class TestRunner implements VitestTestRunner {
}

getImportDurations(): Record<string, ImportDuration> {
const importDurations: Record<string, ImportDuration> = {}
const entries = this.workerState.moduleExecutionInfo?.entries() || []
const { limit } = this.config.experimental.importDurations
// skip sorting if limit is 0
if (limit === 0) {
return {}
}

const entries = [...(this.workerState.moduleExecutionInfo?.entries() || [])]

for (const [filepath, { duration, selfTime, external, importer }] of entries) {
// Sort by duration descending and keep top entries
const sortedEntries = entries
.sort(([, a], [, b]) => b.duration - a.duration)
.slice(0, limit)

const importDurations: Record<string, ImportDuration> = {}
for (const [filepath, { duration, selfTime, external, importer }] of sortedEntries) {
importDurations[normalize(filepath)] = {
selfTime,
totalTime: duration,
Expand Down
1 change: 1 addition & 0 deletions test/cli/test/reported-tasks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const it = baseTest.extend<{
],
includeTaskLocation: true,
logHeapUsage: true,
experimental: { importDurations: { limit: 10 } },
})
expect(collectedTestModules).toHaveLength(1)
await use(ctx!)
Expand Down
Loading
Loading