mcpbeat

Perf

microsoft/perf

Speed and memory performance rules for Rust crates, the Node addon, webui-framework, and webui-router.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
73
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/microsoft/webui --skill perf

The instruction itself

13 sections, as written by the author

Performance

WebUI's value proposition is speed and low memory usage. Every change to core Rust crates, @microsoft/webui-framework, or @microsoft/webui-router must be evaluated through two lenses: throughput (how fast) and memory (how little).

Server memory is not cheap. Client memory is not unlimited. Every allocation that can be avoided is a win on both sides.

Use this skill when modifying any performance-sensitive code across the stack.

Rust - speed rules

These apply to webui-handler, webui-state, webui-expressions, webui-parser, webui-protocol, webui-ffi, and webui-node.

  • No format!() in writer output. Use sequential writer.write() calls. format! allocates a temporary String every invocation.
  • No .to_string() on Cow. Write Cow<str> directly to avoid defeating zero-copy.
  • No collect::<Vec<_>>() on splits. Iterate path.split('.') directly. Collecting allocates a Vec for sequential access.
  • No redundant scans. Use first_part.len() == path.len() instead of path.contains('.').
  • No String::from(ch) in escape loops. Use ch.encode_utf8(&mut buf) with a [u8; 4] stack buffer, or batch contiguous safe chars into a single write.
  • No format! in hex parsing. Use direct arithmetic ((hi_nibble << 4) | lo_nibble) instead of u8::from_str_radix(&format!(...)).
  • No per-request template re-parsing. Pre-parse at protocol load time and reuse.
  • No silent unwrap_or defaults. If binding_stack.pop() returns None, that's a protocol error - propagate it, don't mask it.
  • Keep cold error/diagnostic construction off the hot path. Mark error builders #[cold]/#[inline(never)] so they don't inline into hot functions and perturb their code layout. Adding cold error code with no hot-path work has regressed parse benches ~4-5% purely via layout - keep per-element fast-paths inlinable and push the cold fallback out-of-line. See skills/diagnostics/SKILL.md §7.

Rust - memory rules

  • No cloning large state trees. Use evaluate_with_resolver with a closure. Cloning duplicates the entire JSON tree in memory.
  • No cloning HashMaps for scope. Save/restore only the overwritten key on loop iteration. A HashMap clone copies every entry.
  • No cloning String values for read-only access. Use s.as_str() for Value::String branches; only create owned strings for Number/Bool via a scratch buffer or Cow.
  • Pre-allocate buffers. Use Vec::with_capacity / String::with_capacity when size is known or estimable. For HTML output, 4096 bytes is a reasonable starting point.
  • Prefer &str and slices over owned types. Pass by reference when the callee only reads. Move clone decisions to the caller.
  • Use Cow<'_, str> when a value is sometimes borrowed, sometimes owned. Avoids unconditional allocation.
  • No deep-cloning protocol or state per request. Use Arc<T> with clone-on-write or snapshot swapping.
  • Cap memory for untrusted inputs. File reads during discovery must have size limits. A 100MB HTML file should not cause OOM.

TypeScript - @microsoft/webui-framework rules

These apply to packages/webui-framework (the client-side Web Component runtime).

Speed

  • Single-pass hydration. The framework walks the DOM once to connect all bindings. No multi-pass scanning.
  • Path-indexed targeted updates. When an @observable changes, only bindings referencing that property are visited - not the entire template.
  • DOM cloning over innerHTML. Use cloneNode(true) from cached template fragments. Never use innerHTML for component creation.
  • Direct event listeners. Attach @event bindings to the bound element, never a shared render root. $wireEvents runs once per block instance, so delegating stacks one listener per block on the same node and fires all of them per dispatch — O(N) for no reduction in listener count.
  • Microtask coalescing. Multiple property changes within the same synchronous block batch into a single DOM update via queueMicrotask.
  • Cursor-based repeat reconciliation. <for> block updates use a diff algorithm that only calls insertBefore on nodes that actually moved. Append/prepend/remove are O(1).
  • No for..in on objects. Use Object.keys() with an indexed for loop — faster and prototype-safe without needing Object.hasOwn. Applies to setState, setInitialState, and any code iterating user-provided objects.

Memory

  • No framework in the GC. Minimize object allocations during reactive updates. Reuse binding objects, don't recreate them.
  • Template cache is WeakMap-keyed. Parsed template DOMs are cached per metadata object. When metadata is released (e.g., via Router.gc()), the cache entry becomes GC-eligible.
  • No per-update array allocations. Avoid .filter(), .map(), .slice() in the update hot path. Use index-based iteration.
  • Strip SSR markers after hydration. Comment nodes used as markers are removed from the DOM once wiring is complete - they don't persist as memory overhead.
  • Scope frames are stack-allocated. <for> loop item variables use a linked-list scope chain, not cloned Maps or Objects.

TypeScript - @microsoft/webui-router rules

These apply to packages/webui-router (the client-side SPA router).

Speed

  • Server does route matching. The client does not re-match routes. The server returns the matched chain array; the client diffs old vs new and mounts only changed components.
  • Lazy loading via dynamic import. Route component JS is fetched only on first navigation to that route.
  • Chain diffing, not full remount. Navigating between sibling routes preserves parent components. Only the changed level is remounted.
  • No for..in on objects. Use Object.keys() with an indexed for loop. for..in walks the prototype chain (slow) and requires an Object.hasOwn guard to be safe — Object.keys is both faster and prototype-safe in one call:
   // ✗ Bad: slow, prototype-unsafe without guard
   for (const key in obj) { ... }

   // ✗ Still bad: correct but slower than Object.keys
   for (const key in obj) {
     if (Object.hasOwn(obj, key)) { ... }
   }

   // ✓ Good: fast, prototype-safe, no guard needed
   const keys = Object.keys(obj);
   for (let i = 0; i < keys.length; i++) { ... }

Memory

  • Release unused templates. Router.gc() clears cached component templates for routes the user hasn't visited recently. Active route components are never released.
  • Inventory bitmask prevents duplicate downloads. The server tracks which component templates the client already has via a bitmask. Re-navigation never re-sends templates.
  • Minimal state per navigation. Route-scoped state means the JSON partial contains only what the active route needs, not the full app state.

Measuring

Server benchmarks

cargo bench -p microsoft-webui --bench contact_book_bench          # full run
cargo bench -p microsoft-webui --bench contact_book_bench -- --test # quick validation
cargo xtask bench all                                               # all Rust crates
cargo xtask bench node-addon                                        # Node/V8/N-API boundary

Compare Render/1000 P50 before and after. For Node changes, save with

--save-baseline before and compare with --baseline before. Verify output

Bytes is unchanged (same HTML = correct behavior).

Client performance

window.addEventListener('webui:hydration-complete', () => {
  for (const entry of performance.getEntriesByType('measure')) {
    if (entry.name.startsWith('webui:hydrate:')) {
      console.log(`${entry.name}: ${entry.duration.toFixed(1)}ms`);
    }
  }
});

What to report

When making a performance-related change, report:

  • Before/after benchmark numbers (P50 latency, throughput)
  • Allocation count delta if measurable
  • Output size unchanged (proves correctness)
  • Memory profile for memory-related changes (heap snapshots, RSS delta)

How to use it

Copy the folder

Take microsoft/perf from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.