-
Notifications
You must be signed in to change notification settings - Fork 0
perf: expand benchmark coverage to ~68 BaseUI components #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
1c4dac0
feat(benchmarks): add TIER_1_OPTIONS and TIER_2_OPTIONS presets
joshuapare a65898b
feat(benchmarks): add shared data factories for benchmark tests
joshuapare 2500b2e
feat(benchmarks): add ResizeObserver and IntersectionObserver jsdom s…
joshuapare 29ec28d
refactor(benchmarks): update existing benchmarks to use TIER_2_OPTIONS
joshuapare 341c215
feat(benchmarks): retrofit DataTable with rerender + mountMany benchm…
joshuapare 399871c
feat(benchmarks): add Tier 1 data-heavy component benchmarks
joshuapare c3270f8
feat(benchmarks): add Tier 1 complex interactive benchmarks
joshuapare 6e4e62a
feat(benchmarks): add Tier 2 form input benchmarks
joshuapare 99d1f19
fix(benchmarks): add Portal wrapper to Autocomplete and Combobox benc…
joshuapare d66394f
fix(benchmarks): add Portal to AlertDialog and matchMedia stub for Ed…
joshuapare 867c3b8
perf(benchmarks): address PR review feedback and expand mountMany cov…
joshuapare 460a6ae
feat(benchmarks): add Reassure render-count profiling infrastructure
joshuapare 481aba4
fix(benchmarks): address PR review feedback on Reassure perf tests
joshuapare File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| /** | ||
| * Compare Reassure performance results between baseline and current runs. | ||
| * | ||
| * Usage: | ||
| * pnpm perf:baseline # run on base branch, writes .reassure/baseline.perf | ||
| * pnpm perf # run on current branch, writes .reassure/current.perf | ||
| * pnpm perf:compare # compare and output report | ||
| */ | ||
| import { compare } from '@callstack/reassure-compare'; | ||
|
|
||
| await compare({ | ||
| baselineFile: '.reassure/baseline.perf', | ||
| currentFile: '.reassure/current.perf', | ||
| outputFile: '.reassure/output.json', | ||
| outputFormat: 'all', | ||
| }); |
49 changes: 49 additions & 0 deletions
49
packages/benchmarks/src/__perf__/base-ui/Accordion.perf-test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { test } from 'vitest'; | ||
| import { measureRenders } from 'reassure'; | ||
| import { fireEvent, screen } from '@testing-library/react'; | ||
| import { Accordion } from '@omniview/base-ui'; | ||
| import { ThemeWrapper } from '../utils/theme-wrapper'; | ||
|
|
||
| function FiveItemAccordion() { | ||
| return ( | ||
| <Accordion animation="none"> | ||
| {Array.from({ length: 5 }, (_, i) => ( | ||
| <Accordion.Item key={i} id={`item-${i}`} title={`Section ${i}`}> | ||
| Content for section {i} | ||
| </Accordion.Item> | ||
| ))} | ||
| </Accordion> | ||
| ); | ||
| } | ||
|
|
||
| test('Accordion: mount 5 items', async () => { | ||
| await measureRenders(<FiveItemAccordion />, { wrapper: ThemeWrapper }); | ||
| }); | ||
|
|
||
| /** | ||
| * Expanding one section should NOT re-render sibling sections. | ||
| * This catches shared-context over-notification patterns. | ||
| */ | ||
| test('Accordion: expand one section', async () => { | ||
| await measureRenders(<FiveItemAccordion />, { | ||
| wrapper: ThemeWrapper, | ||
| scenario: async () => { | ||
| fireEvent.click(screen.getByText('Section 2')); | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * Expanding then collapsing — tests the full toggle cycle. | ||
| * Render count should be similar to a single expand. | ||
| */ | ||
| test('Accordion: expand then collapse', async () => { | ||
| await measureRenders(<FiveItemAccordion />, { | ||
| wrapper: ThemeWrapper, | ||
| scenario: async () => { | ||
| const trigger = screen.getByText('Section 2'); | ||
| fireEvent.click(trigger); | ||
| fireEvent.click(trigger); | ||
| }, | ||
| }); | ||
| }); |
22 changes: 22 additions & 0 deletions
22
packages/benchmarks/src/__perf__/base-ui/Button.perf-test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { test } from 'vitest'; | ||
| import { measureRenders } from 'reassure'; | ||
| import { Button } from '@omniview/base-ui'; | ||
| import { ThemeWrapper } from '../utils/theme-wrapper'; | ||
|
|
||
| /** | ||
| * Button is the simplest interactive component — its render count | ||
| * serves as the baseline for all other components. | ||
| * Expected: 1 render on mount, 0 issues. | ||
| */ | ||
| test('Button: mount', async () => { | ||
| await measureRenders(<Button>Click me</Button>, { wrapper: ThemeWrapper }); | ||
| }); | ||
|
|
||
| test('Button: mount with decorators', async () => { | ||
| await measureRenders( | ||
| <Button startDecorator={<span>+</span>} endDecorator={<span>→</span>}> | ||
| Action | ||
| </Button>, | ||
| { wrapper: ThemeWrapper }, | ||
| ); | ||
| }); |
49 changes: 49 additions & 0 deletions
49
packages/benchmarks/src/__perf__/base-ui/Checkbox.perf-test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { test } from 'vitest'; | ||
| import { measureRenders } from 'reassure'; | ||
| import { fireEvent, screen } from '@testing-library/react'; | ||
| import { Checkbox } from '@omniview/base-ui'; | ||
| import { ThemeWrapper } from '../utils/theme-wrapper'; | ||
|
|
||
| test('Checkbox: mount', async () => { | ||
| await measureRenders( | ||
| <Checkbox.Item defaultChecked={false}>Accept terms</Checkbox.Item>, | ||
| { wrapper: ThemeWrapper }, | ||
| ); | ||
| }); | ||
|
|
||
| test('Checkbox: toggle checked', async () => { | ||
| await measureRenders( | ||
| <Checkbox.Item defaultChecked={false}>Accept terms</Checkbox.Item>, | ||
| { | ||
| wrapper: ThemeWrapper, | ||
| scenario: async () => { | ||
| const checkbox = screen.getByRole('checkbox'); | ||
| fireEvent.click(checkbox); | ||
| }, | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| /** | ||
| * Critical scenario: toggling one checkbox in a list. | ||
| * If memoization is missing, ALL checkboxes re-render on a single toggle. | ||
| */ | ||
| test('Checkbox: toggle one in list of 20', async () => { | ||
| const items = Array.from({ length: 20 }, (_, i) => `Option ${i}`); | ||
| await measureRenders( | ||
| <div> | ||
| {items.map((label) => ( | ||
| <Checkbox.Item key={label} defaultChecked={false}> | ||
| {label} | ||
| </Checkbox.Item> | ||
| ))} | ||
| </div>, | ||
| { | ||
| wrapper: ThemeWrapper, | ||
| scenario: async () => { | ||
| const checkboxes = screen.getAllByRole('checkbox'); | ||
| fireEvent.click(checkboxes[0]!); | ||
| }, | ||
| }, | ||
| ); | ||
| }); |
59 changes: 59 additions & 0 deletions
59
packages/benchmarks/src/__perf__/base-ui/Dialog.perf-test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { useState } from 'react'; | ||
| import { test } from 'vitest'; | ||
| import { measureRenders } from 'reassure'; | ||
| import { fireEvent, screen } from '@testing-library/react'; | ||
| import { Dialog, Button } from '@omniview/base-ui'; | ||
| import { ThemeWrapper } from '../utils/theme-wrapper'; | ||
|
|
||
| function DialogWithTrigger() { | ||
| const [open, setOpen] = useState(false); | ||
| return ( | ||
| <> | ||
| <Button onClick={() => setOpen(true)}>Open Dialog</Button> | ||
| <Dialog open={open} onClose={() => setOpen(false)} size="md"> | ||
| <Dialog.Title>Confirm Action</Dialog.Title> | ||
| <Dialog.Body>Are you sure you want to proceed?</Dialog.Body> | ||
| <Dialog.Footer> | ||
| <Button onClick={() => setOpen(false)}>Cancel</Button> | ||
| <Button onClick={() => setOpen(false)}>Confirm</Button> | ||
| </Dialog.Footer> | ||
| <Dialog.Close /> | ||
| </Dialog> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Mount cost includes the trigger but NOT the dialog body (open=false). | ||
| * Render count should be minimal — just the trigger + null dialog. | ||
| */ | ||
| test('Dialog: mount (closed)', async () => { | ||
| await measureRenders(<DialogWithTrigger />, { wrapper: ThemeWrapper }); | ||
| }); | ||
|
|
||
| /** | ||
| * Opening the dialog triggers portal mount, backdrop, and content render. | ||
| * High render counts here signal excessive setState chains in useEffect. | ||
| */ | ||
| test('Dialog: open', async () => { | ||
| await measureRenders(<DialogWithTrigger />, { | ||
| wrapper: ThemeWrapper, | ||
| scenario: async () => { | ||
| fireEvent.click(screen.getByText('Open Dialog')); | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * Full open/close cycle. Render count should be ~2x the open-only count. | ||
| * Significantly more signals cleanup-related re-renders. | ||
| */ | ||
| test('Dialog: open then close', async () => { | ||
| await measureRenders(<DialogWithTrigger />, { | ||
| wrapper: ThemeWrapper, | ||
| scenario: async () => { | ||
| fireEvent.click(screen.getByText('Open Dialog')); | ||
| fireEvent.click(screen.getByText('Cancel')); | ||
| }, | ||
| }); | ||
| }); |
63 changes: 63 additions & 0 deletions
63
packages/benchmarks/src/__perf__/base-ui/Input.perf-test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { test } from 'vitest'; | ||
| import { type ChangeEvent, useState } from 'react'; | ||
| import { measureRenders } from 'reassure'; | ||
| import { fireEvent, screen } from '@testing-library/react'; | ||
| import { Input, TextField } from '@omniview/base-ui'; | ||
| import { ThemeWrapper } from '../utils/theme-wrapper'; | ||
|
|
||
| test('Input: mount', async () => { | ||
| await measureRenders( | ||
| <Input> | ||
| <Input.Label>Username</Input.Label> | ||
| <Input.Control placeholder="Enter username" /> | ||
| </Input>, | ||
| { wrapper: ThemeWrapper }, | ||
| ); | ||
| }); | ||
|
|
||
| test('TextField: mount', async () => { | ||
| await measureRenders( | ||
| <TextField> | ||
| <TextField.Label>Email</TextField.Label> | ||
| <TextField.Control placeholder="Enter email" /> | ||
| </TextField>, | ||
| { wrapper: ThemeWrapper }, | ||
| ); | ||
| }); | ||
|
|
||
| /** | ||
| * Controlled input wrapper so each fireEvent.change triggers a state update | ||
| * and re-render, simulating real per-keystroke behavior. | ||
| */ | ||
| function ControlledInput() { | ||
| const [value, setValue] = useState(''); | ||
| return ( | ||
| <Input> | ||
| <Input.Label>Username</Input.Label> | ||
| <Input.Control | ||
| placeholder="Enter username" | ||
| data-testid="input" | ||
| value={value} | ||
| onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)} | ||
| /> | ||
| </Input> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Typing in an input field — each keystroke should cause minimal re-renders. | ||
| * High counts here signal uncontrolled→controlled mismatches or expensive | ||
| * parent re-renders from onChange. | ||
| */ | ||
| test('Input: type 10 characters', async () => { | ||
| await measureRenders(<ControlledInput />, { | ||
| wrapper: ThemeWrapper, | ||
| scenario: async () => { | ||
| const input = screen.getByTestId('input'); | ||
| const chars = 'helloworld'; | ||
| for (let i = 0; i < chars.length; i++) { | ||
| fireEvent.change(input, { target: { value: chars.slice(0, i + 1) } }); | ||
| } | ||
| }, | ||
| }); | ||
| }); |
67 changes: 67 additions & 0 deletions
67
packages/benchmarks/src/__perf__/base-ui/Select.perf-test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { test } from 'vitest'; | ||
| import { measureRenders } from 'reassure'; | ||
| import { fireEvent, screen } from '@testing-library/react'; | ||
| import { Select } from '@omniview/base-ui'; | ||
| import { makeOptions } from '../../utils/factories'; | ||
| import { ThemeWrapper } from '../utils/theme-wrapper'; | ||
|
|
||
| const options = makeOptions(20); | ||
|
|
||
| test('Select: mount with 20 options', async () => { | ||
| await measureRenders( | ||
| <Select defaultValue={options[0]!.value}> | ||
| <Select.Trigger> | ||
| <Select.Value placeholder="Choose..." /> | ||
| </Select.Trigger> | ||
| <Select.Portal> | ||
| <Select.Positioner> | ||
| <Select.Popup> | ||
| <Select.List> | ||
| {options.map((opt) => ( | ||
| <Select.Item key={opt.value} value={opt.value}> | ||
| {opt.label} | ||
| </Select.Item> | ||
| ))} | ||
| </Select.List> | ||
| </Select.Popup> | ||
| </Select.Positioner> | ||
| </Select.Portal> | ||
| </Select>, | ||
| { wrapper: ThemeWrapper }, | ||
| ); | ||
| }); | ||
|
|
||
| /** | ||
| * Opening a Select triggers portal mount, positioner layout, and popup render. | ||
| * High render counts here signal excessive context notifications. | ||
| */ | ||
| test('Select: open dropdown', async () => { | ||
| await measureRenders( | ||
| <Select defaultValue={options[0]!.value}> | ||
| <Select.Trigger> | ||
| <Select.Value placeholder="Choose..." /> | ||
| </Select.Trigger> | ||
| <Select.Portal> | ||
| <Select.Positioner> | ||
| <Select.Popup> | ||
| <Select.List> | ||
| {options.map((opt) => ( | ||
| <Select.Item key={opt.value} value={opt.value}> | ||
| {opt.label} | ||
| </Select.Item> | ||
| ))} | ||
| </Select.List> | ||
| </Select.Popup> | ||
| </Select.Positioner> | ||
| </Select.Portal> | ||
| </Select>, | ||
| { | ||
| wrapper: ThemeWrapper, | ||
| scenario: async () => { | ||
| const trigger = screen.getByRole('combobox'); | ||
| fireEvent.click(trigger); | ||
| await screen.findByRole('listbox'); | ||
| }, | ||
| }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| ); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/benchmarks/package.json | head -30Repository: omniviewdev/ui
Length of output: 1449
🏁 Script executed:
Repository: omniviewdev/ui
Length of output: 1675
🏁 Script executed:
Repository: omniviewdev/ui
Length of output: 977
🏁 Script executed:
Repository: omniviewdev/ui
Length of output: 2164
🏁 Script executed:
Repository: omniviewdev/ui
Length of output: 589
🏁 Script executed:
Repository: omniviewdev/ui
Length of output: 186
Use cross-platform approach for perf scripts.
Lines 10-11 use POSIX-only shell syntax (
rm -fandREASSURE_OUTPUT_FILE=...env assignment) that will fail on Windows. Since the project already uses Node scripts for similar tooling (perf-compare.mjs), move the cleanup and environment setup into a Node script to ensureperfandperf:baselinework consistently across platforms.Cross-platform solution
"scripts": { "bench": "vitest bench --run", "bench:browser": "vitest bench --config vitest.config.browser.ts --run", - "perf": "rm -f .reassure/current.perf && vitest --run --config vitest.config.perf.ts", - "perf:baseline": "rm -f .reassure/baseline.perf && REASSURE_OUTPUT_FILE=.reassure/baseline.perf vitest --run --config vitest.config.perf.ts", + "perf": "node scripts/run-perf.mjs current", + "perf:baseline": "node scripts/run-perf.mjs baseline", "perf:compare": "node scripts/perf-compare.mjs" },📝 Committable suggestion
🤖 Prompt for AI Agents