Unifyl — a dual-pane file manager for macOS · Blog

AppKit asks every menu to update on every keypress. My app said yes.

August 2026 · by the developer of Unifyl

The bug report, as filed by my own hands: hold the down arrow in a file list and the cursor stutters its way down instead of gliding. A file manager that can't scroll smoothly is a car that can't idle, so this jumped the queue.

I assumed the table view. It's the obvious suspect — thousands of rows, custom drawing, selection updates on every step. So I profiled a key-repeat session, opened the heaviest main-thread stack, and found… menu code. With everything included, the main thread was 61% busy during key repeat, and the hot stacks weren't under the table view at all. They were under the menu bar.

Nobody was looking at the menu bar. No menu was open. Why is the menu bar doing work at all while I hold an arrow key?

Key equivalents are resolved eagerly

Here's the mechanism. Every keyDown that reaches a window also has to answer the question "is this a menu shortcut?" AppKit answers it by walking the menu tree — and before it checks a menu's key equivalents, it gives that menu's delegate a chance to bring the items up to date. Which means:

menuNeedsUpdate(_:) is not "the user opened a menu". It also fires for every menu in the bar, on every key press, during key-equivalent resolution. Hold a key at ~30 repeats per second with nine top-level menus, and your delegate runs a few hundred times per second — while the user believes the menu bar is idle.

My delegate was a decorator: it walks menu items and annotates the ones that need a Pro badge, using localized titles to match items. The implementation was written with "the user just opened a menu" in mind, so being called meant: re-decorate all nine menus, and rebuild the localized title tables it matches against — from scratch, every call. Per keypress. The work was correct; the assumption about how often it runs was fiction.

The fix is boring, which is the point

Result: main-thread occupancy during key repeat went from 61% to 2%, and the cursor glides. No table-view changes at all — the obvious suspect was innocent the whole time.

Notes for the next person