frontend-render-performance
Skill1 starsChanged 37 days agoMITView on GitHub
UI slower the longer it runs? Find O(n) per-event work.
In this repository34 other files it gives its agents.
- agent-branch-integrationSkill
- agent-config-syncSkill
- blocked-page-recoverySkill
- brand-retrofitSkill
- clickable-concept-prototypeSkill
- codebase-inspectionSkill
- commit-workSkill
- contrastive-refactor-demoSkill
- custom-domain-emailSkill
- delegate-coding-taskSkill
- delegated-agent-supervisionSkill
- delivery-verificationSkill
- domain-dns-cutoverSkill
- first-run-onboardingSkill
- github-pages-deploySkill
- hermes-cron-jobsSkill
- hermes-messaging-channelsSkill
- hermes-on-oracle-free-tierSkill
- hermes-provider-credentialsSkill
- isolated-worktree-testingSkill
- measurement-validitySkill
- multi-layout-previewSkill
- new-project-scaffoldingSkill
- parallel-agent-fanoutSkill
- premortemSkill
- recall-and-claim-verificationSkill
- single-source-training-materialsSkill
- stakeholder-decision-elicitationSkill
- static-site-vps-deploySkill
- supabase-auth-debuggingSkill
- windows-background-process-hygieneSkill
- windows-desktop-automationSkill
- windows-performance-tuningSkill
- worktree-analysisSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 7 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 18
Use when a UI is reported as slow in a way that involves time or accumulation rather than a single slow action:
- Distinctive: line 27
Do not use for a single slow operation with a fixed cost (one heavy query, one huge bundle, one blocking import).
- Very specific: line 110
When memo() is already present and still re-rendering, the fault is almost never in the memoized component.
- Distinctive: line 150
Every fix here is a caching or identity-freezing change, so each one can introduce the opposite bug: content that stops updating when it …
- Distinctive: line 167
Remove the fix (see the dirty-repo recipe below).
- Distinctive: line 168
Run the gate. It must fail, with numbers matching the original symptom.
- Distinctive: line 181
Use plain file copies instead — no stash, no index, nothing to reconcile:
- Very specific: line 192
If a stash operation does go wrong, do not improvise: git status immediately, git restore --source=HEAD --staged --worktree -- <the files …
- Distinctive: line 228
If the repo carries a spec/requirement-ID convention (an AGENTS.md mapping directories to owner docs), a performance fix is a behavioral …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Frontend Render Performance
When to Use
Use when a UI is reported as slow in a way that involves time or accumulation rather than a single slow action:
- "gets slow after a while of using it"
- "laggy while it's streaming / typing / loading"
- "fine when I reload, then degrades again"
- "slow on long conversations / big lists / after many items"
- a page that profiles fine on an empty state and badly on a real one
Do not use for a single slow operation with a fixed cost (one heavy query, one huge bundle, one blocking import). That is a different problem: profile the operation directly.
The Signature
Degrades with use + reload fixes it = per-event work that scales with
accumulated client state.A reload resets in-memory state, so the symptom resets too. That single fact rules out most server-side and network explanations and points at a handler, effect, or render path whose cost is a function of how much has piled up.
The usual shape: something fires at high frequency (a stream token, a keystroke, a scroll, a websocket message, a timer) and does O(n) work over everything received so far. At n=5 nobody notices. At n=200 the app is unusable, and the user describes it as "slow after a while".
Phase 1 — Measure the Curve, Not the Clock
Do not start by reading code looking for something that looks expensive. The expensive thing is usually cheap per unit and merely repeated n times.
Build a harness that drives the high-frequency event over a synthetic history of several sizes and records a count, not a duration.
history size: 1 5 10 25 50
renders/event: 4 12 22 52 102 <- linear in n. This is the bug.Read the shape:
| Shape across sizes | Meaning |
| Flat | Correct. Not your problem. |
| Grows with n | O(n) per event — the target. |
| Grows faster than n | Nested repetition; often a memo miss inside a loop. |
Why counting, not timing
Wall-clock in a test environment is close to useless for this. In one investigation the same unchanged code timed 387.7 ms/frame, then 6.3 ms/frame, then 260.6 ms/frame on consecutive runs — swings far larger than the effect being measured, from JIT warm-up, GC, and other load on the machine. The render count in those same runs was stable and exactly linear.
So count the invariant:
- renders (React), patches (DOM), queries, allocations, requests, iterations
- a count is deterministic, survives a noisy machine, and reads as a complexity claim rather than a vibe
- it also becomes a permanent gate; a millisecond threshold in CI is a future
flake,
expect(renders).toBe(0)is not
Counting renders in React
Replace the children under test with memo() spies. A memo() component
re-renders if and only if one of its props changed identity — which is
exactly the property being investigated, so the spy is the measurement.
const counts = { item: 0 };
vi.mock('@/components/chat', async () => {
const R = await import('react');
const MessageItem = R.memo(function Spy(): React.ReactElement {
counts.item += 1;
return R.createElement('div');
});
return { MessageItem, /* ...other exports stubbed */ };
});Then render, reset the counters (so mount cost is excluded), drive N events, and read the counters. Vary history size and compare shapes.
Profiler-based alternatives (React DevTools, <Profiler>) are fine for
exploration but harder to assert on in CI. Prefer spies for the permanent gate.
Phase 2 — Find What Defeats the Memo
When memo() is already present and still re-rendering, the fault is almost
never in the memoized component. It is upstream, in a prop whose identity
is rebuilt every event even though its value is unchanged.
Work outward from the child:
- List every prop the child receives.
- For each, ask: is this the same object as last event?
- The offenders are usually functions, arrays, and objects created inline or recreated by a hook whose dependency array contains a per-event value.
See references/react-memo-defeaters.md for the full catalog with the two
real-world cases that motivated this skill, plus the defenses that were already
in place and still were not enough.
The headline traps:
- A
useCallbackthat lists a per-event array in its deps. The array is new on every state dispatch, so the callback is new, so every child holding it re-renders. Read the array from a ref instead and drop the dep. - A container rebuilt around stable contents.
memocompares the prop, not the prop's elements. Stabilizing every item in an array does nothing if the array itself is reconstructed.
Phase 3 — Fix at the Source, Re-measure
Fix one leak, re-run the harness, confirm the curve flattened, then look for the next. Fixing several at once means you cannot attribute the improvement, and some "fixes" do nothing.
Re-measure with the same harness and report before/after side by side:
50 turns of history, per streaming token:
before: 102 MessageItem renders, 50 ThinkingBlock renders
after: 2 MessageItem renders, 0 ThinkingBlock rendersGuard against over-caching
Every fix here is a caching or identity-freezing change, so each one can introduce the opposite bug: content that stops updating when it genuinely should. Write that test too. A cached group whose own data is still arriving must still re-render:
// live block changed -> renders; the 10 settled ones -> do not
expect(counts.thinking).toBe(1);A frozen UI is a worse bug than a slow one. Do not ship the speedup without this test.
Phase 4 — Make It a Gate, and Prove It Can Go Red
A performance test that has never failed is decoration. Before believing it:
- Remove the fix (see the dirty-repo recipe below).
- Run the gate. It must fail, with numbers matching the original symptom.
- Restore the fix. It must pass.
Without step 2 you have no evidence the test observes the thing you fixed.
Safely A/B a fix in a repo you do not own
git stash is the obvious tool and it is a trap in a shared repo. In one
session git stash push -- <path> followed by git stash pop partially
applied an unrelated pre-existing stash, leaving conflict markers (UU)
across eight untouched files. The repo had eight stashes from other branches;
popping is not scoped the way you expect.
Use plain file copies instead — no stash, no index, nothing to reconcile:
T="$TMPDIR" # or $LOCALAPPDATA/Temp on Windows
cp path/to/File.tsx "$T/File.mine.tsx"
git checkout HEAD -- path/to/File.tsx # baseline
<run the gate: expect RED>
cp "$T/File.mine.tsx" path/to/File.tsx # restore
<run the gate: expect GREEN>If a stash operation does go wrong, do not improvise: git status immediately,
git restore --source=HEAD --staged --worktree -- <the files you did not touch>,
then confirm the stash list is still the length it was. Say so in your report.
Baseline the other gates before attributing them
A repo-wide lint / typecheck / spec-check that returns hundreds of problems
after your edit says nothing until you know the count before it. Measure both:
# with your change
pnpm run lint 2>&1 | grep -E '^✖' > /tmp/mine.txt
# baseline
cp <changed files> /tmp/ && git checkout HEAD -- <changed files>
pnpm run lint 2>&1 | grep -E '^✖' > /tmp/base.txt
# restore, then diff the two"215 problems, byte-identical to baseline, none in the files I touched" is a verified claim. "Those look pre-existing" is a guess. This protects you in both directions: you neither inherit someone else's red build nor claim a clean run you never had.
And do run the type checker on your own new test file. In this session it caught
a reference to a messagesRef that did not exist yet — a real bug in the fix
that would have crashed the feature the moment a user clicked the button.
Working Rules For This User
- Confirm the target branch before editing. He may name one explicitly
("faça na main") while the working copy sits on another. Check
git branch --show-currentfirst and switch; do not edit wherever you landed. - Exhaust the investigation, then report short. Chase every open thread before coming back, but lead the reply with the before/after table and the root cause. Do not narrate the steps he already watched scroll past.
- Do not commit or push unless asked. Leave the tree dirty and say so.
- If the repo carries a spec/requirement-ID convention (an
AGENTS.mdmapping directories to owner docs), a performance fix is a behavioral change: it needs a new ID and a filled coverage column citing the gate you just wrote.
Pitfalls
| Pitfall | Do this instead |
| Reading code hunting for something "expensive" | Measure the curve first; the culprit is cheap × n |
| Timing in a test environment | Count operations; read the shape across sizes |
| One input size | Three or more, or you cannot see O(n) vs O(1) |
Trusting memo() because it is present | It compares prop identity; verify with a spy |
| Stabilizing array contents but not the array | memo compares the container |
| Fixing several leaks at once | One at a time, re-measure between |
| Shipping a cache without a staleness test | Frozen UI is worse than slow UI |
| A perf test never seen failing | Revert the fix and watch it go red |
git stash to A/B in a shared repo | Copy to temp + git checkout HEAD -- |
| "Those lint errors were already there" | Diff against the same gate run at HEAD |
Files
references/react-memo-defeaters.md— catalog of what breaksReact.memo, with real cases, the defenses that were already present and insufficient, and the harness pattern for counting renders.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

