본문으로 건너뛰기
cccskills
무료GitHub에 공개

perf

How to find and fix performance problems in Shift's desktop app. Use when something is slow, choppy, janky, or laggy (scrubbing, dragging, editing, undo, opening fonts), when profiling or measuring, when adding or reviewing perf tests, and before claiming a change made something faster.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md7.1 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

/perf — Measuring and fixing slowness in Shift

Measure the build users run, find what started the work, fix that, and measure again with the same command. A guess that "this looks expensive" has been wrong more often than right here; a number with a cause attached has not.

1. Measure a packaged production build

Never draw conclusions from pnpm dev. React's development build adds jsxDEV, prop validation, and render logging that made up about half of every dev profile and pointed at the wrong code.

pnpm profile:build      # native bridge (release) + renderer minified like a release, with source maps and component names (~1 min warm)
pnpm profile:desktop --font <path> [--glyph A] [--scenario scrub] [--axis Weight] [--seconds 8] [--cpu out.cpuprofile]

profile:desktop launches the packaged app with a throwaway profile, opens the font and glyph, drives the scenario with real pointer input, and prints:

  • Frame times (p50/p90/p99, frames over 33 ms) — the number the user feels.
  • React renders per commit, and for each re-rendered subtree the component and the hook slot or context that started it. This is what tells you what to fix.
  • A source-mapped CPU profile — busy time, top self time, top inclusive time in Shift code. Open the saved .cpuprofile in Chrome DevTools for flame charts.

Notes:

  • The packaged app is in apps/desktop/out/<platform>-<arch>/ (linux-unpacked/shift on Linux). Older out/Shift-* directories are stale Electron Forge output; ignore them.
  • Packaged builds disable Node's inspector fuse, so Playwright's _electron.launch cannot attach. The script uses Chromium's --remote-debugging-port and connectOverCDP instead.
  • On a remote or headless Linux session the app needs the desktop's environment (DISPLAY/WAYLAND_DISPLAY, XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS), and must not inherit a Nix LD_LIBRARY_PATH.
  • Linux needs Chromium's Vulkan feature for WebGPU; without a GPU adapter the glyph grid falls back and the numbers mean nothing.
  • Add a scenario to scripts/profile-desktop.mjs rather than writing a one-off script, so the next person can rerun your measurement.

2. Frontend: find what started each render

Read the "Renders started by" list top-down:

  • hook#N — hook slot N in that component changed. useSignalState (and useSyncExternalStore) take two slots each, useContext takes none, so count use* calls to map a slot to a line.
  • context {…} — a context value changed; the keys identify which one. A context built from a high-frequency signal re-renders every consumer on every change.
  • A component starting the same number of renders as there were commits is subscribed to the thing being scrubbed or dragged. Ask whether it shows that value. If not, it's the bug.

Patterns that caused real regressions (rules in /signals):

  • Re-doing work on every change that a signal already tracks — the catalog re-opened the editor's glyph on every location change although its render model follows the location signal. This re-rendered the whole editor.
  • New-but-equal values — new Set(...) written each step to a signal compared by Object.is. Give the signal an equals.
  • Wide contexts — Editor read the catalog context just for openedGlyph. Narrow it to its own context or read the specific cell.
  • Hidden but mounted views — the home grid stays mounted behind the editor to keep its WebGPU atlas, and kept re-laying out on every scrub step. Hold its inputs while hidden.
  • Subscribing for a value used only on an event — read cell.peek() in the handler.
  • Main-process pushes that resend unchanged data — the menu bar was re-sent after every command. Compare before sending.

A healthy weight scrub on Inter Italic (Ryzen 5 5500U, Linux): p99 frame ≈ 16.8 ms, about 60 component renders per commit, main thread about 25% busy. Use that as a reference for "fixed".

3. Backend: the workspace process and the store

Edits, undo, glyph loads, and atlas preparation run in the workspace utility process through the Rust bridge. pnpm profile:desktop --scenario open prints main's per-phase atlas timings (SHIFT_PROFILE_SLUG_ATLAS); --scenario undo times edit-coordinator round trips. pnpm build does not rebuild the native bridge — profile:build does, and a Rust change measured without it is measuring the old code.

A .shift document opens with a recovery overlay: every table is read through a temp view that UNION ALLs the recovery rows with the canonical rows, filtered by tombstones, replacement markers, and parent visibility (crates/shift-store/src/recovery/views.rs). SQLite pushes WHERE terms into those views, so a key lookup searches each arm by index; it does not push join terms. So in store queries:

  • Never join two store tables on a key. Express the second table as col IN (SELECT …). referenced_glyph_ids_for_glyphs took 117 ms as a join and 14 ms as an IN subquery on Inter; dependent_glyph_ids_for_layers, which runs after every edit, undo, and redo, took 172 ms and 8 ms.
  • Compare a query against main.<table> to see what the overlay costs. A large gap means the view is being scanned rather than searched; EXPLAIN QUERY PLAN shows it as CO-ROUTINE <view> followed by AUTOMATIC COVERING INDEX.
  • nix develop --command sqlite3 -readonly <file>.shift runs queries and EXPLAIN QUERY PLAN against a real document; the recovery views only exist inside the app's connection, so compare the canonical query there and the merged one through a store test.
  • Store tests use canonical tables, so they will not catch this. Measure on a real document opened with recovery.

4. Prove it

  • Rerun the exact same profile:desktop command and report before/after numbers in the commit message.
  • A React re-render regression gets a budget in apps/desktop/e2e/render-budget.spec.ts (visual project, so it runs in CI without a GPU). It counts mean component renders per commit during a weight scrub and a marquee with the same counter as profile:desktop (e2e/fixtures/renderCounter.mts); a failure lists what started the renders. Budgets sit about 15% over the measured value, so a new subscription on those paths must raise one on purpose.
  • If the problem can regress silently, add or extend a spec in apps/desktop/e2e/perf.spec.ts (pnpm --filter @shift/desktop test:e2e:perf, run through the shift-remote-e2e skill rather than on a user's desktop). It records p50/p95 against perf-baseline.json.
  • Moving a rule between TypeScript and @shift/wasm, or keeping one in TypeScript for speed, needs both paths measured on the same workload, including packing and unpacking at the boundary (see "Choosing a route" in crates/shift-wasm/docs/DOCS.md).
  • Note in the commit which platform you measured on; Linux and Windows can be several times slower than macOS for the same code, so "fast on my Mac" proves nothing.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

commit

무료

Canonical rules for writing git commits in the Shift codebase. Use whenever the user asks to commit, stage and commit, create a pull request that requires commits, or draft a commit message. Enforces Conventional Commits, release-note quality, concise subjects, and logical commit boundaries.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

shift-editor/shift3592026년 10월 19일 업데이트

dead-code

무료

Find and remove dead code (unused files, exports, class members) using Knip as a candidate generator, then verify each candidate through AST-level analysis and interface tracing before removing anything. Use when the user asks to clean up unused code, find dead code, or reduce the codebase.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

shift-editor/shift3592026년 10월 19일 업데이트

docs

무료

Update or create DOCS.md files for Shift subsystems. Use this skill whenever the user asks to update docs, refresh documentation, create a DOCS.md, write module documentation, or says "update docs for X". Also trigger after completing a large feature when Claude.md says to update docs — check if any DOCS.md in the affected subsystem needs refreshing.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

shift-editor/shift3592026년 10월 19일 업데이트

Adversarially fact-check DOCS.md files against the actual source code, verifying every concrete claim rather than trusting structure checks. Use when the user asks to audit docs, verify documentation accuracy, check whether docs are still true, or on a scheduled documentation review. This is the semantic layer the mechanical checkers cannot cover.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

shift-editor/shift3592026년 10월 19일 업데이트

issue

무료

Canonical rules for finding, creating, and updating Shift GitHub issues. Use whenever the user asks to file, create, open, update, triage, or search for an issue, or when substantial work needs an issue before a pull request. Prevents duplicates and defines acceptance criteria and pull-request closure semantics.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

shift-editor/shift3592026년 10월 19일 업데이트

jsdoc

무료

Add or revise source-level JSDoc for Shift APIs. Use this skill before writing or editing documentation comments for exported classes, methods, constructors, domain data structures, render frames, reactive state, or any API where caller intent, side effects, lifetime, ownership, or nullability are easy to misunderstand.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

shift-editor/shift3592026년 10월 19일 업데이트

shift-editor의 스킬 모두 보기

이 스킬의 문제 신고