Skip to content

[poc][autocomplete] Improve speed of opening the menu#48534

Draft
silviuaavram wants to merge 1 commit into
mui:masterfrom
silviuaavram:perf/autocomplete-open
Draft

[poc][autocomplete] Improve speed of opening the menu#48534
silviuaavram wants to merge 1 commit into
mui:masterfrom
silviuaavram:perf/autocomplete-open

Conversation

@silviuaavram
Copy link
Copy Markdown
Member

@silviuaavram silviuaavram commented May 13, 2026

Some perf measurements and fix proposal, on Autocomplete open action.

To repro, click Autocomplete, then click outside (blur). The issue seems to be the double render, which increases the time the dropdown takes to open. Batching the state updates for open + pristine with focused avoids the double render. This issue is reported in Chrome, as a task that took too long. This is a non-production screenshot, production takes less.

Screenshot 2026-05-13 at 10 02 30

The production version is more likely the one below. However, this is on a Macbook Pro, other devices might not be this fast.

Before After
Screenshot 2026-05-13 at 10 09 17 Screenshot 2026-05-13 at 10 08 39

The PR contains a test that calculates averages of the open action, and a tears down by clicking outside to close the menu. Running the test with and without the fix:

  // avg: 18.54ms
  // min: 10.31ms
  // max: 28.77ms

  // avg: 22.26ms
  // min: 16.90ms
  // max: 51.48ms

@code-infra-dashboard
Copy link
Copy Markdown

Deploy preview

https://deploy-preview-48534--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+9B(0.00%) 🔺+2B(0.00%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@silviuaavram silviuaavram changed the title [perf experiment] Autocomplete open speed [poc][autocomplete] Improve speed of opening the menu May 13, 2026
@mj12albert
Copy link
Copy Markdown
Member

Codex suggestion on the benchmark, it didn't even occur to me React.Profiler could be used like this, I usually only use the devtools extension

A robust version should count React commits in the browser, not measure Playwright timing from Node.

Core idea:

  1. Wrap the Autocomplete fixture in React.Profiler.
  2. Expose a small window.__autocompletePerf API.
  3. In Playwright, arm profiling immediately before the click.
  4. Wait until the listbox is visible.
  5. Wait one or two animation frames so the focus event / layout effects have flushed.
  6. Read the commit log and assert commit count, or at least compare master vs patch.

Sketch:

import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';

declare global {
  interface Window {
    __autocompletePerf: {
      reset: () => void;
      start: () => void;
      stop: () => void;
      getCommits: () => Array<{
        phase: 'mount' | 'update' | 'nested-update';
        actualDuration: number;
        commitTime: number;
      }>;
    };
  }
}

const commits: Array<{
  phase: 'mount' | 'update' | 'nested-update';
  actualDuration: number;
  commitTime: number;
}> = [];

let recording = false;

window.__autocompletePerf = {
  reset() {
    commits.length = 0;
    recording = false;
  },
  start() {
    commits.length = 0;
    recording = true;
  },
  stop() {
    recording = false;
  },
  getCommits() {
    return commits.slice();
  },
};

const options = Array.from({ length: 100 }, (_, i) => `Option ${i + 1}`);

export default function OpenPerfAutocomplete() {
  return (
    <React.Profiler
      id="autocomplete"
      onRender={(_, phase, actualDuration, _baseDuration, _startTime, commitTime) => {
        if (recording) {
          commits.push({ phase, actualDuration, commitTime });
        }
      }}
    >
      <Autocomplete
        options={options}
        renderInput={(params) => (
          <TextField
            {...params}
            label="Open perf"
            slotProps={{
              ...params.slotProps,
              htmlInput: {
                ...params.slotProps.htmlInput,
                'data-testid': 'input',
              },
            }}
          />
        )}
      />
    </React.Profiler>
  );
}

Then the e2e side:

it('opens with one Autocomplete commit after mousedown', async () => {
  await renderFixture('Autocomplete/OpenPerfAutocomplete');

  const input = page.getByTestId('input');

  // Ensure clean baseline: closed and unfocused.
  await page.mouse.click(0, 0);
  await page.evaluate(() => window.__autocompletePerf.reset());

  await page.evaluate(() => window.__autocompletePerf.start());
  await input.click();

  await page.getByRole('listbox').waitFor({ state: 'visible' });

  // Let discrete focus/click work and layout-effect follow-up commits flush.
  await page.evaluate(
    () =>
      new Promise<void>((resolve) => {
        requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
      }),
  );

  await page.evaluate(() => window.__autocompletePerf.stop());

  const commits = await page.evaluate(() => window.__autocompletePerf.getCommits());

  expect(commits.filter((commit) => commit.phase === 'update')).toHaveLength(1);
});

The important details:

  • Profiler observes actual React commits, so it detects the thing this PR is meant to remove.
  • Start recording before input.click(), so the mousedown update is included.
  • Wait past listbox visibility, because visibility only proves the open commit happened; the unwanted focus commit can follow.
  • Keep the fixture closed and unfocused before every run.
  • Count commits, not milliseconds. Durations are useful diagnostics, but noisy enough that they should not be the pass/fail condition.

For this PR, I’d expect master to record two update commits on the blurred-input click path: one from mousedown -> open/inputPristine, then one from focus -> focused. The patched version should ideally record one update commit for that path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants