Unity performance optimization. Use when the user asks to optimize a class, view, function, folder or an entire module, or mentions stutter, lag, frame drops, low FPS, jank, overheating, battery drain, GC allocation, memory leaks, DrawCalls, batching, overdraw, slow loading, large build size, Profiler, PerfDog, or crash rates. Also covers pure C# micro-optimization: algorithmic complexity, memory layout, SIMD, and compiler-level tuning.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add tianzhiying/Unity-perf --skill unity-performance --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Unity Performance?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/tianzhiying-unity-performance)More formats (shields.io, HTML) on the badges page.
---
name: unity-performance
description: "Unity performance optimization. Use when the user asks to optimize a class, view, function, folder or an entire module, or mentions stutter, lag, frame drops, low FPS, jank, overheating, battery drain, GC allocation, memory leaks, DrawCalls, batching, overdraw, slow loading, large build size, Profiler, PerfDog, or crash rates. Also covers pure C# micro-optimization: algorithmic complexity, memory layout, SIMD, and compiler-level tuning."
---
# Unity performance optimization
This skill sets the targets, establishes the disciplines, and points at what is easy to miss. The actual optimization ability comes from your own algorithmic repertoire, your understanding of engine internals, and the community practice you know. The reference files are a safety-net sweep before delivery, not a ceiling on what you produce. If every finding in a pass maps to a numbered checklist entry, that usually doesn't mean the code is clean — it means you didn't explore enough.
## 1. Acceptance criteria
Four non-negotiable targets. Every piece of performance work is accepted against them.
- **Frame budget**: 60 FPS means 16.6 ms per frame, 30 FPS means 33.3 ms. Every optimization must answer "where does this frame's time get saved."
- **Zero GC on hot paths**: code that runs every frame (Update, LateUpdate, coroutine bodies, UI refreshes, network packet handling) must hit GC Alloc = 0 B/frame. This is a hard standard, not a best effort.
- **Correct algorithmic order**: hot-path complexity must match the data scale. O(n²) paired with an n that can grow is a defect unless you produce evidence bounding n.
- **Passes on device**: the final measure is measured frame rate, PSS peak, and thermals on the target low-end device — not editor numbers.
## 2. Five iron rules
Violate any one of them and the optimization is redone.
1. Measure before you touch. Take evidence with the Profiler (on device + Development Build) before optimizing, and re-measure the same scene after. Changing code on a hunch is forbidden. The only exception is fixing deterministic anti-patterns that are guaranteed wins and need no data.
2. Measure on the target device. The editor adds its own overhead, and Mono and IL2CPP behave differently. Editor numbers don't count.
3. Don't change things to show off. Run every change through three applicability questions — can you argue its correctness (give the boundary cases)? How large is the blast radius? Is the readability cost worth it? If it fails, say plainly "the current implementation is good enough."
4. Optimization must not create new debt. Indexes, lookup tables, and heaps introduced by an algorithmic fix must be pre-allocated, reused, and capped. You may not fix an O(n²) problem by creating a "new dictionary every frame" problem. Accept on both columns: time and GC Alloc.
5. Verify configuration claims. Whenever a conclusion depends on a compile switch, a macro, or build configuration (whether `[Conditional]` gets stripped, whether the logging macro is defined, Debug vs Release differences, Scripting Define values), check the project's actual configuration before concluding. Hypothetical claims like "it should get stripped in the release build" are forbidden — the real state of a macro frequently invalidates the premise of an entire analysis.
## 3. Autonomous exploration (the primary engine for finding problems)
A checklist cannot exhaust the problem space of real code. Run any code through the six thinking operators below first; use the checklist only as the last step before delivery, to catch omissions.
**Lifecycle symmetry audit.** For everything that gets created, registered, started, or borrowed (objects, event subscriptions, coroutines, Tasks, timers, timeout closures, pooled objects, message handlers, tweens, runtime-instantiated GameObjects and loaded assets), ask who is responsible for destroying, unregistering, stopping, or returning it along four paths: normal completion, exceptional abort, repeated trigger, and host destruction. No symmetric counterpart is a finding. When you do find one, also audit the ordering and coverage of the cleanup: calling `Clear()` on a container before iterating it to return items, nulling a field before using it to finalize, exception paths that bypass the return, returning only "the current one" while missing the rest of the collection — **the existence of return code does not mean every borrowed object gets returned.** These gaps don't throw and don't crash; they show up only as monotonically growing resource use. For creation on a repeat-trigger path, add one more question: "on the Nth trigger, where are the previous N-1?" Hiding is not the end of a lifecycle: hidden ≠ destroyed ≠ returned.
**Interrogate the underlying cost.** For any call whose cost is unclear, ask "what does this actually do underneath," then read the implementation or check the docs to confirm — is it JNI? A synchronous disk flush? A stack capture? A full-tree walk? Rebuilding every binding? No confirmation, no conclusion. Fan-out state-refresh chains must be followed one hop in: those `SetXXX` methods that run "per action or per round × N instances" usually hide their real cost inside the in-house setters, helpers, and partial components they call (a SetActive off-then-on rebuild, a fresh coroutine each time, GetComponent round trips, dead branches — all outside the main file). **A clean main file does not mean a clean chain.** If you haven't followed it in, you may not conclude "no issues" on that chain.
**Timeline simulation.** Run the code in your head at three scales: one user action (the cascade ledger — how many times does it actually execute?), one business cycle (a match or a login — repetition and races), and a one-hour session (accumulation, containers that only grow, high-water marks). Problems invisible to static line-by-line reading become visible under simulation.
**Reachability and zero-reference determination.** A type or member can be referenced in far more ways than one: serialized attachment (GUIDs in prefabs and scenes), static code references (`new`, type names, generic arguments), runtime dynamic attachment (`AddComponent<T>` / `GetComponent<T>`, often buried in utility extension methods), reflection and string-name lookup, config tables and asset paths and event registries, and indirect retention via `link.xml`, Analyzers, and Timeline/Animator events. Checking one or two of these and declaring "dead code" will condemn live code. **The evidence bar for declaring something dead must be higher than for declaring it alive** — recommending deletion is irreversible, while declaring it alive costs you at most a little missed savings. When unsure, write "no attachment reference found; confirm dynamic attachment points before deleting." When enumerating reference carriers, the most commonly missed ones are the module's own sibling files and indirect dispatch at the framework layer (events bubbling upward, base classes auto-adding components during initialization, message dispatch).
This operator has an equally important reverse use: before you account for a path and rank it, confirm it is actually reached in the current build. Declaring something dead answers "can this be deleted"; reachability also decides "is this worth optimizing" — for a path that never executes, however precisely you compute the cost, you are pointing the budget at thin air and crowding out real findings. The causes are mundane: an enum's value range commented down to a single member, a second config or registration commented out wholesale, a switch constant that is always the same value, `if (current == target) return;` being always true over a single-value domain. Before writing "the ledger for one action," ask "can the value that triggers it even be produced in the current code?" By the same token, views and components whose entry points are all commented out or have zero callers don't go on the optimization list.
**Contract consistency check.** The moment you find a convention (a pool's borrow/return interface, a `HasInstance` guard, register/unregister pairing, a flag's set/reset, a comment saying "must go through this entry point"), grep every call site and check adherence. A convention existing does not mean the convention is followed — the exemplar and the violation are often in the same file.
**Adversarial self-check.** Before delivering, ask yourself: what would a stronger reviewer find that I didn't write down? Which dimension has zero findings from me — is there genuinely nothing, or did I not look? Fold the answers back into the report before handing it over.
Order discipline: checklist comparison, coverage matrices, closure statements, and self-check declarations are omission-catching tools, not a way to produce findings. **Explore freely first, then verify systematically** — start as if no checklist exists and read problems out of the code using the operators and your instincts; then go item by item against the checklist and the matrix to catch what you missed; write the declaration last. Doing it in the other order finds only what is already on the checklist.
## 3.2 Coverage dimensions
For each dimension, ask yourself "did I bring all of my knowledge to bear at this level?"
**Algorithms and data structures always come first** — the order-of-magnitude payoff is the largest. Don't settle for picking at API usage line by line. Abstract the business logic down to its algorithmic essence first (hand-type search is subsequence enumeration, connectivity is union-find, bulk range updates is a difference array, sliding-window statistics is a monotonic queue, dependency resolution is a topological sort), then use your full algorithmic repertoire to present complexity-compared options. Watch for redundant computation at the same time — recomputing invariant results, processing an incremental problem in full, polling a low-frequency event, iterating in the wrong direction. The corresponding weapons are caching plus dirty flags, incremental updates, event-driven design, and reversed precomputation. When n is small, constants and cache friendliness matter more than complexity.
**User-perceived latency** requires you to stop looking at code purely through CPU and GC eyes. Fixed wait windows (double-click detection, debounce delays, animations that lock input), serial waits (having to wait for a response before reacting to a tap), and artificial throttling never show up in the Profiler's GC column, yet they directly determine how the game feels. When reviewing an interaction path, compute the "from user action to visible response" time ledger separately — a few hundred milliseconds of fixed delay is often worth far more than saving a few KB of allocation.
**Memory and allocation**: catch every implicit allocation on a hot path (boxing, closures, LINQ, strings, temporary collections, non-NonAlloc APIs). For systematic remediation, hand off to the sibling skill csharp-zero-gc.
**Engine layer**: DrawCalls and batching, overdraw, material instantiation traps, physics configuration, asset loading (sync to async, Addressables, spreading across frames), Instantiate pooling. Proactively consider the dividends of newer Unity versions (GPU Resident Drawer, GPU occlusion culling, STP upscaling, Awaitable, InstantiateAsync, and newer features) and recommend what the user's version supports. Before concluding "no issues at the engine layer," you must have specifically checked: Animator usage (Rebind abuse, string parameters, how animation waits are done), tween lifecycles (linkage to the host, Kill on destroy, callbacks after destruction), the failure paths of polling waits, and the generation and cancellation of one-shot timeouts and delayed closures (request timeout fallbacks, delayed calls, repeatedly triggerable wait coroutines — one by one). Also particle and VFX residency (cleanup is layered — on leaving a scene clear instances but keep templates; on a low-memory callback clear everything; don't treat "we cleaned up" as a pass). Not seeing Find/GetComponent does not mean the engine layer is clean.
**UI (UGUI/TMP)**: Canvas rebuild storms, static/dynamic separation, virtualized scroll lists, the rebuild cost of SetActive, TMP mesh updates. Frame drops when opening a view and list stutter should be investigated here first.
**Parallelism and pushing work down**: for large volumes of homogeneous computation (pathfinding, batched raycasts, procedural generation, large unit counts), proactively recommend Job System + Burst + NativeArray, and don't be afraid to recommend ECS where it fits.
**Frontier and ecosystem**: community-proven zero-allocation and high-performance libraries (UniTask, ZString, ZLinq, MemoryPack, LitMotion, and newer options) should be introduced proactively where they fit, with the selection rationale stated.
If the user asked only about A but you can see significant opportunities in dimensions B and C in the code, you must raise those too, labeled as proactive findings and kept distinct from what was asked.
## 3.3 The five pseudo-optimization checks
Run every finding through these before reporting it. Pseudo-optimizations have nothing to do with "found nothing"; they are all "found something that doesn't hold up," and they cluster in these five places.
1. **Reachability** — does the code being optimized actually execute? Are there call sites (grep the whole repo; watch for reflection, hot-update, and serialized UnityEvent calls)? Are the serialized fields bound (an unassigned reference in a prefab means the branch that depends on it never runs)? Is the branch condition always true or always false (a static switch field assigned in exactly one place across the repo makes the other branch dead)? Is there a gate upstream (an early `return`, an API unsupported on the platform, an undefined macro, something that only takes effect in a development build)? **If you can't answer "under what conditions does this run," you may not report it.**
2. **Already handled below** — has the engine, framework, importer, or compiler already done this? For example: TMP's `text` setter already early-outs on an equal value; `SetActive` with the same value early-outs; the texture importer's max-size setting already downsamples at import time (regardless of source size); the platform's default compression format is already ASTC; an existing static memo already coalesces concurrent requests; a newer BCL has already eliminated that enum boxing. Confirm the layer below hasn't already done it before adding a dirty check or a cache.
3. **Self-consistent prescription** — two questions. Does the fix solve the very fact you stated? (Classic failure: you established that the logging macro *is* defined and that `[Conditional]` therefore doesn't strip anything, then still prescribed "add `[Conditional]`"; or you wrote that the image format has no alpha, then counted disabling the alpha channel as a gain.) And does the fix itself hold up — can that delegate field bind an instance method, does the API exist, will it compile?
4. **Order-of-magnitude truth** — is the frequency per frame or event-driven (dirty-flag-coalesced refresh is not per frame)? Is it a configured cap or the actual peak (particle cost tracks live particle count, not the cap field)? Is it on-disk size or runtime footprint? Is the table dozens of rows or tens of thousands? This check **only lowers the benefit tier; it does not delete entries.** Order-of-magnitude judgments are the most subjective, and the cost of a false kill exceeds the cost of a false pass. You may delete an entry only when you both have a concrete order-of-magnitude counter-proof and the target is not on a per-frame or high-frequency event path; otherwise downgrade, keep it, and note the real magnitude.
5. **Side effects of the fix** — four classes of risk that are easy to under-report. Reusing an object can break an upstream "reference-equal, so skip the refresh" check, leaving entries permanently showing stale content. Caching config-table data in a static field breaks when a hot update swaps the table instance wholesale, leaving the cache pointed at the old table forever. Bypassing an encapsulation (an extension method, a utility function) drops the rules inside it, such as forced async loading or per-row degradation fallbacks. Eliminating a closure by moving the comparison token into an instance field breaks async callbacks that relied on the captured epoch/path/id to decide "is this response stale" — once it's an instance field the staleness test becomes a tautology and an old response overwrites newer content (the correct fix is to compare against the callback's own parameters, which naturally carry the values from when the request was issued). **Stating a risk honestly costs you nothing; failing to report one does.**
Column discipline: only things that are "faster after the change" go on the performance list. Anything that "should be fixed but won't be faster" goes to the correctness red flags in section 8 — flipping a mock switch to real adds a network round trip, setting the log level correctly adds a stack capture; mixing these into the performance list gets them judged as negative optimizations.
## 3.4 Coverage inventory
Before delivery, declare "checked" or "not applicable" for each layer. The gap between covering all six layers and only sweeping the code layer is not ability — it's not having thought to look. Confirming each layer costs almost nothing.
| Layer | What to catch | How to get evidence |
|---|---|---|
| Code | Allocation, algorithmic complexity, call frequency, lifecycle symmetry | Source + call chain |
| Prefabs | Redundant components, Mask vs RectMask2D choice, raycastTarget ratio, Canvas partitioning, resident particles and Animators | `.prefab` grep counts |
| Texture / audio / video import | Max size and platform overrides, mipmaps, Read-Write; audio loadType, bitrate and channels; video transcoding | Bulk grep over `.meta` |
| Project settings | Scripting Defines (is the logging macro really stripped), graphics API, default compression format, stripping, incremental GC | Assets under `ProjectSettings` |
| Build configuration | Whole-directory collection pulling zero-reference assets into the build; whether bundles are split by language and purpose | AssetBundle collector config |
| Framework layer | Shared code this module touches (lists, events, loaders) — one fix multiplied across the whole project | Walk up the call chain |
Establish the execution-layer premises first, then prune based on them: before touching anything, confirm the scripting backend (IL2CPP / Mono / hot-update interpreter), whether unsafe is allowed, the C# language version, and the target platform's graphics API. These premises can eliminate entire classes of recommendation in one shot (hand-rolled loops replacing the BCL can be a regression under an interpreter; without unsafe, Span and stackalloc are off the table entirely) — far cheaper than checking every item and only then discovering it doesn't apply.
## 4. Diagnostic methodology
Start from the symptom; don't guess. This tree is a fallback starting point — your own diagnostic ability is the primary engine.
```
Low FPS / stutter → check the Profiler Timeline first: what is the main thread waiting on?
├─ Gfx.WaitForPresent dominates → GPU bound → engine-layer dimension (engine-rendering.md as fallback)
├─ User scripts dominate → CPU logic bound → algorithm dimension + hot-path dimension (csharp-hotpath.md as fallback)
├─ Canvas.SendWillRenderCanvases/BuildBatch dominates → UI rebuild → UI dimension (ui-ugui-tmp.md as fallback)
└─ Physics.Processing dominates → physics configuration
Periodic spikes → nine times out of ten it's GC.Collect → sort by GC Alloc to find the allocation source
Stutter only during a specific action (opening a view / spawning enemies / loading) → burst cost → pooling / async loading / spreading across frames
Memory climbing / crashes → leak → two-snapshot comparison in Memory Profiler (profiling.md)
```
## 5. On-device acceptance and live-ops remediation
Whenever on-device acceptance, a pre-launch pass, or live issue remediation is involved, recommendations must carry both of these measures. An editor-only view is not acceptable.
On-device baselines (PerfDog / WeTest): state the acceptance metrics precisely — mean FPS, jank (including the BigJank definition), PSS peak, temperature and current draw — with baselines per target device tier, and mark which metric each fix is expected to improve. Live-ops remediation (Bugly / CrashSight): interpreting crash rate, ANR, OOM, and stutter reports; symbolicating IL2CPP builds; top-N remediation. Live symptoms must flow back into the diagnostic tree for reproduction and localization. For the detailed definitions and baseline tables, see [references/wetest-bugly-fieldops.md](references/wetest-bugly-fieldops.md).
For the Tencent-derived field criteria (the UPA memory three-way split, the CPU attribution four-way split, long-message stutter criteria, and the five OOM dimensions), see [references/tencent-optimization-playbook.md](references/tencent-optimization-playbook.md).
## 5.2 The C# extreme-optimization ladder and runtime guardrails
The ladder only answers "which layer to squeeze first." What you use at each layer is up to you; frontier techniques are fair game as long as they clear the evidence and order-of-magnitude checks.
| Layer | Direction | Typical gain | Fallback reference |
|----|------|---------|------|
| L0 Algorithms | Complexity, precomputation, result caching, incremental updates | 10–1000× | [baseline-antipatterns.md](references/baseline-antipatterns.md) group A |
| L1 Allocation | Zero GC: pooling, Span, stackalloc, struct | Eliminates GC spikes, 2–10× | See csharp-zero-gc for the engineering plan |
| L2 Layout | Cache friendliness: SoA, struct field ordering, sequential access, false-sharing elimination | 2–20× | [memory-gc-internals.md](references/memory-gc-internals.md) |
| L3 Compiler | Inlining, devirtualization, bounds-check elimination, branch elimination, IL2CPP check stripping | 1.2–3× | [jit-il2cpp-tricks.md](references/jit-il2cpp-tricks.md) |
| L4 Vectorization | SIMD (Burst and Unity.Mathematics preferred), parallelism | 4–16× | [span-collections-simd.md](references/span-collections-simd.md) |
| L5 unsafe | Pointers, fixed buffers, bypassing safety checks | 1.1–2× | Final chapter of jit-il2cpp-tricks.md |
**Work the layers in order.** Reaching for L4 while L2 is undone (data jumping randomly through memory) is wasted — the CPU is waiting on memory and the vector units idle. Caching and skip-style optimizations need two levels of follow-up: after skipping unchanged frames, can the changed frames themselves be diffed (apply only the delta instead of replaying everything)? And the "no external interference" assumption a cache rests on must either be proven by auditing the full call surface or given an explicit invalidation API as an escape hatch — one of the two is mandatory. For benchmarking method, see [references/benchmarking.md](references/benchmarking.md).
Runtime guardrails must be confirmed before you start, and not merely down to "IL2CPP or Mono" — confirm down to how the target assembly actually executes: is it in the hot-update assembly list of a hot-update framework (HybridCLR, ILRuntime, xLua) — check the framework's settings asset and the asmdef GUIDs; is it constrained by an AOT generic pre-generation manifest (when a hot-update framework's AOT metadata supplement list is empty, introducing a new value-type generic instantiation crashes on device — even "use an enum as a dictionary key" is out); does the asmdef have `allowUnsafeCode` enabled? Within one build, AOT-compiled assemblies and interpreted assemblies can differ by an order of magnitude in the unit cost of loop iteration and allocation. Miss this layer and every multiplier that follows is wrong.
| Technique | .NET 8+ | Unity IL2CPP | Unity Mono | Hot-update interpreter |
|------|:---:|:---:|:---:|:---:|
| Span / stackalloc / ArrayPool | ✅ | ✅ | ✅ | Constrained by `allowUnsafeCode` |
| AggressiveInlining / sealed devirtualization | ✅ | ✅ | ⚠️ weak | ❌ no inlining |
| SIMD: hardware intrinsics | ✅ | ❌ | ❌ | ❌ |
| The Unity answer for SIMD: Burst + Unity.Mathematics | — | ✅ | ✅ | — |
| Function pointers `delegate*` | ✅ | ✅ (2021.2+) | ✅ | ⚠️ measure it |
| Dynamic codegen (Emit / expression trees) | ✅ | ❌ forbidden under AOT | ✅ | ❌ |
| New value-type generic instantiation | ✅ | ⚠️ needs AOT manifest | ✅ | ❌ crashes without supplemental metadata |
**Guardrails prevent wrecks; they don't set a ceiling.** The matrix reflects runtime reality as of writing. If you know a newer version has lifted one of these limits, go with your knowledge and cite the version.
## 6. Reference files
These files are accumulated, validated criteria and solutions, used as a review safety net, for citing standard implementations, and for keeping definitions and numbers consistent. They do not stop you from proposing something better than what's in them.
| File | When to read it |
|------|-----------|
| [baseline-antipatterns.md](references/baseline-antipatterns.md) | The final omission sweep after exploration is done (not the main review loop): group A algorithms and redundancy, group B API and lifecycle anti-patterns, numbered for citation |
| [profiling.md](references/profiling.md) | You can't locate the bottleneck; you need to teach the user how to capture data |
| [csharp-hotpath.md](references/csharp-hotpath.md) | Point-by-point identification of hot-path allocation sources and their standard fixes |
| [jobs-burst-dots.md](references/jobs-burst-dots.md) | Job / Burst / ECS implementation patterns and pitfalls |
| [engine-rendering.md](references/engine-rendering.md) | Rendering, physics, memory, build size, loading, and Unity 6 features |
| [ui-ugui-tmp.md](references/ui-ugui-tmp.md) | UGUI and TMP specific standard solutions |
| [wetest-bugly-fieldops.md](references/wetest-bugly-fieldops.md) | PerfDog metric definitions and baseline tables; the Bugly live-ops remediation loop |
| [tencent-optimization-playbook.md](references/tencent-optimization-playbook.md) | Tencent's official field-criteria library |
Sibling skills: hand system-level zero-GC engineering plans to csharp-zero-gc, and C# coding standards plus the orchestrated review protocol to code-standards. The limits of pure C# at the runtime and compiler level are folded into section 5.2 of this skill.
## 7. Execution strategy
**Target shape recognition.** The XXX in "optimize XXX" may be a function, a class, or a file — but it may equally be a folder, a directory, or a module name. When the target is a directory or spans multiple files, first use Glob plus size statistics to define the file list, rank by hotspot priority (size × per-frame-or-per-event path × call frequency), then work through it. The deliverable is a global summary plus per-file detail — cross-file common problems are merged into a single entry listing every file and line number they hit, and the per-file section lists only what is unique to that file. Repeating the same pattern file by file is forbidden.
**Parallel acceleration.** When subagent or parallel-task tooling is available, multi-file work and independent dimensions must be evaluated for parallelism (fan out by file, run independent dimensions in parallel, prepare review and verification in parallel). Subagent prompts must carry this skill's discipline points or name the skill file path so the subagent loads it. Don't parallelize small single-file tasks — the orchestration overhead exceeds the gain. The orchestration discipline is: dispatch in one wave, wait for all of them, synthesize personally. The deliverable can only be the final report itself; ending a turn with "waiting for subagents to return" is a non-delivery.
**Token economy.** During localization use structural scans and precise greps; don't read irrelevant files line by line. The report contains findings, fixes, and evidence only — don't paste large blocks of unmodified source. Cite the number for a repeated class of problem instead of expanding it again. Reuse existing test fixtures. Scale the spec to the size of the job — for small targets (roughly under 3k lines), shrink the report and the verification breadth in step, verify configuration and cross-file facts only where they directly support a specific finding, and use a compact single-line format for the self-check declaration. Saving tokens must never come at the cost of missed detection: the declaration must be complete, but complete is not the same as long-winded. Batch your evidence gathering: capture configuration, serialization, and macro evidence with a single multi-pattern grep or one bulk read, merge multiple line numbers in the same file into one read, and list "the evidence I need to verify" up front so you can collect it in one wave — rather than looking things up one at a time as they occur to you.
## 8. Output specification
Review code across three dimensions in order: algorithms and redundancy, allocation and zero-GC, engine and structure. Each finding produces "file:line + problem (may cite a baseline checklist number) + fix + estimated gain," sorted by gain, with functions that are both time-expensive and allocation-heavy first. Algorithmic fixes must explain how the auxiliary structures achieve zero new allocation.
Give algorithmic modeling its own section: essential problem → current complexity → candidate approaches (including ones you modeled yourself) → recommendation and rationale.
Sort fixes into three tiers: deterministic gains with small diffs, do them directly; safe cleanups next; structural refactors get only a recommendation and a blast-radius assessment, pending confirmation. Conclusions from static scanning are marked "needs on-device Profiler confirmation."
**Panorama before action.** For "optimize XXX" tasks, deliver the complete review first (all three dimensions covered, the algorithmic modeling section written, operator-driven exploration done thoroughly), and only then start making tier-one changes. Scanning and editing simultaneously, or stopping early on a first-tier easy win, is forbidden. **The main deliverable must be the largest-magnitude finding** — when an algorithmic or structural finding exists (call-chain multipliers, O(n²) redundancy, cascading rebuilds), tier-one items like caching GetComponent or deleting dead code may only appear as an appendix and must not occupy the body of the report. Writing the 100× problem into the summary and the 1× change into the body is a layout accident and, more importantly, a judgment accident. Tier three isn't acted on, but the analysis depth must not be discounted: give the full multiplier, the choke point, the blast radius, and the verification plan. "Pending confirmation" is not a license to write shallowly.
Verification: re-measure after the fix and compare frame time, GC Alloc, and DrawCalls; where on-device acceptance is involved, add the PerfDog metrics.
An **"already meets the bar"** section lists the correct practices you found (already cached, already pooled, already event-driven) as "keep, do not touch," so the next optimization pass doesn't revert them. Before listing one, run a consistency audit: check that pattern across every comparable call site in this file and this module. A guard that only takes effect at some call sites becomes a *finding*, not an "already meets the bar" — praising the exemplar while letting the violation through is a review accident. Then decompose the cost: caching a material copy ≠ having no copy, setting a tween to auto-Kill ≠ the object being collected, adding a dirty check ≠ not executing. For each one, ask "which cost did this measure eliminate, which remain, and do the remaining ones matter." If you can't answer all of that, you may not write "already meets the bar." When citing configuration, read all the relevant fields — reading half of them is how something costly gets classified as free.
The verification depth behind a blanket claim like "no issues" must match the depth used for findings (including partial files and the internals of in-house components being called). If you didn't verify down to that layer, write only "did not verify this layer." **An unverified-layer declaration is not a disclaimer; it is the boundary of your conclusion**: any "already meets the bar" or "no issues" that depends on a layer you declared unverified must be downgraded to a conditional statement or dropped entirely. Honestly listing unverified layers while still issuing unconditional claims elsewhere is worse than doing no self-check at all. Likewise, "count is 0" in static assets proves only that these files don't contain it — it does not prove the runtime scene doesn't. Subtrees instantiated at runtime (cosmetics, effects, and skins loaded from player data) and components a base class or framework `AddComponent`s onto existing nodes during initialization (sorting, masking, raycasting, and layout infrastructure are most often added this way) must be checked too; if you can't check them, write the conclusion as "no X inside the static prefab." The latter is especially insidious: the nodes you scanned genuinely lack the component, but a base class adds it the moment it enters the scene — and every conclusion derived from "it isn't there" (batch domains, iteration counts, rebuild area) inverts.
**"Unverified" may only be used for layers you genuinely cannot reach, never for things you simply didn't read.** Config assets in the repo, data files you can decode, paths and GUIDs you can look up, and first-party modules with available source are all readable. The test is: for every unverified declaration you write, first answer "which evidence-gathering method did I try, and where did it block me" (source not in the repo / binary with no parsing basis / only determinable at runtime). If you can't name the blocker, you didn't do it — go read it. Then look back and self-check: did I issue an unconditional claim about that same layer somewhere else? Before citing a file path, confirm it actually opens; writing a wrong path and then supplying detailed content from "that file" is fabricating evidence.
A **"correctness red flags"** section stands alone: correctness hazards found incidentally during the review must be reported, in their own section, never mixed with performance items — missing guards around async and coroutine lifecycles, null checks in the wrong order, misleading indentation and suspected bugs, resources not returned on exception paths, test and integration code leaking into production paths (hard-coded test domains or mock addresses not wrapped in `#if UNITY_EDITOR`), unguarded parsing that deserializes before checking the error code, sensitive information going into logs. They are not this skill's primary responsibility, but ignoring them means letting a live incident through. Note the problem and the fix only; whether to fix them along the way is the user's call.
**Off-checklist findings and the self-check declaration.** Problems that no checklist number covers must still be reported, tagged with which thinking operator produced them. End the report with a "self-check" section that goes through the six operators × each coverage dimension and declares, item by item, "checked with findings / checked, nothing found / not checked (with the reason)" — so that "checked and there's nothing" is distinguishable from "never looked."
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!