How General Translation keeps visual surfaces fast with no change to how they look.
pnpm skills:install gt-performance --project <dir>Run it from a Prototemplate checkout. It links skills/gt-performance into the project’s .claude/skills and .agents/skills; --copy vendors the folder instead, and --dry-run prints each step first.
General Translation (GT) runs live canvas and WebGL material (the dither globe, the glyph field, the studio field, the horizon shader) on generaltranslation.com, on the dashboard and in Prototemplate, and those pages must also load fast. This skill is the procedure for making any such surface fast without changing a pixel: the rule, measurement, quality tiers, engine lifecycles, shrinking code, Lighthouse, live values on static pages and budgets. Page specifics stay with their skills. gt-website references/docs.md ("Performance") holds the docs investigation and the Lighthouse commands for generaltranslation.com. gt-motion section 6 and DESIGN.md section 11 hold the engine contract, gt-dither holds the engines and their copies, and gt-cloud's react-best-practices covers React rendering.
Paths are relative to a Prototemplate checkout ($PROTOTEMPLATE) unless marked gt-cloud ($GT_CLOUD, GT's product monorepo, origin/main on 2026-10-05). A direction is one of the design-exploration pages under Prototemplate's /d/<slug>. The two scripts in this folder's scripts/ run from a checkout that has playwright-core (Prototemplate does) and find Chrome for Testing the way gt-aesthetic's measure-type.mjs does.
<dl> whose cells held buttons became divs, with the CSS re-anchored on existing data attributes;<h3> became a styled <div>, so the outline steps in order;div became <main>, with no new wrapper;width and height as ratio hints, and CSS keeps their size;<body suppressHydrationWarning> silenced the hydration error that extensions such as Grammarly cause by stamping attributes on the body;robots.ts was added.color-contrast and target-size went to Kevin as design changes.
.sgdh-ins-mark (apps/landing/src/components/landing/home/sections/translate-window.css) is a 25 px invisible button whose 11 px badge is painted by ::before.ditherAtlasRows in src/lib/glyph-field.ts). The moving rain still flickered, and Kevin removed the dither from it: "the dither is making it flicker every time it moves. remove the dither" (2026-08-06). Depth now reads by size and alpha (TIER_COVER), and the dither stays for static consumers.src/lib/glyph-field.ts, start()), and createDitherLoop paints one frame synchronously before its first rAF.requestIdleCallback (the prepared sample set in src/lib/glyph-field.ts). Measure each event twice, the first use and the second. Move anything heavier on the first use into idle time, behind a loading state, or into a warm-up render before the user can act.scripts/frame-probe.mjs. It samples requestAnimationFrame intervals for a fixed window after a warmup, and prints the median, p95, worst interval, slow share, long tasks, WebGL contexts created and lost, canvases in the DOM, any glyph-field tier, the GL renderer and the load average before and after (node skills/gt-performance/scripts/frame-probe.mjs <url> --runs 3; the flags and more examples are in references/recipes.md). Headless Chrome on Kevin's Mac draws WebGL on the real GPU (the renderer column reads ANGLE Metal). Where it reads SwiftShader, rerun with --headed. The Claude in-app browser pane reports document.hidden as true, pauses requestAnimationFrame and leaves shared-engine canvases blank, so never measure motion there. Discard the first probe after a dev server compiles a route: on 2026-08-07 a first probe of the hero read 21 fps with a 1.6 s frame, and unthrottled repeats read 118 fps.uptime, or the load frame-probe prints). The machine is shared with parallel agent pipelines: in September 2026 they pushed the one-minute load to between 100 and 370, and it read 280 on the 18-core machine while this skill was written. Every timing reads slow under that load. Rerun at low load, take the before and after runs back to back under similar load, and compare deltas. frame-probe warns when the load is above the core count.Kevin asked on 2026-08-07 whether the glyph rain matched the device's real performance or only its responsive width. It keyed off width alone, so a wide window on weak hardware got the heaviest tier and a narrow window on a fast laptop got the lean one. Kevin approved a frame-time governor the same day, and Prototemplate's src/lib/glyph-field.ts holds the pattern:
narrow field (under 880 px) starts on the lean tier. Every quality site reads one combined flag, lean = narrow || govTier >= 2.GOV_WINDOW) after a 2.5 s warmup (GOV_WARMUP_MS), skipping any interval over 900 ms as a hidden tab (GOV_GAP_MS). A window trips when 35% or more of its frames exceed 22 ms (GOV_TRIP, GOV_BUDGET_MS), which is a sustained cadence below about 45 fps.resize(), so the backing store, the atlas and every blit change together. Tier 2 binds the frame path to the lean pool (560 of 1,280 glyphs) and waits until no glyph is in flight, so no particle leaves the draw path mid-move.canvas.dataset.gfTier for probes. Proof on 2026-08-07: a CDP throttle at 12x stepped to tier 2 within the window and an unthrottled 10 s control never tripped. Reproduce with frame-probe's --cpu 12 (its tiers column).fpsCap: 30, which skips half the paints. Its governor counts every rAF tick, so a skipped draw never reads as a slow frame (src/app/d/production/sections/ink-field.ts, 2026-08-07).packages/ui/src/lib/glyph-field.ts has the zoom watcher (section 4) and no governor, and Prototemplate's copy has the governor and no zoom watcher. A performance fix goes into every copy in the same round, the rule gt-dither section 2 sets for the studio field's copies.Chrome keeps about 16 live WebGL contexts, and past that a field draws black with no error, so a shared engine holds one context per engine module per session, stops its loop when no field is on screen or the tab is hidden, and a canvas that grows never shrinks back. The full rules, the incidents behind them and the files that follow them are in references/webgl-lifecycles.md.
dither.ts, and 2.8 KB gzipped for what sign-in imports. Then read the built chunks for a string the minifier keeps, such as a GLSL uniform name (the loop is in references/recipes.md). One source module can still land in several route chunks: on the 2026-10-01 Prototemplate build the horizon shader sits in four chunks of 11.7 to 19.3 KB gzipped. Report the count with the sizes.horizon-field.ts. One shared src/lib/horizon-field.ts replaced both.SHADER NOTES block beside the literal (src/lib/horizon-field.ts, src/lib/studio-field.ts). The horizon fragment literal went from 6.5 KB to 4.4 KB, and the module from about 7.4 KB to 4.6 KB gzipped.#version 300 es stays the literal's first characters (` const FRAG = #version 300 es ``). A compaction that put a newline before it failed the compile silently, and only the DOM fallback disc rendered until the screenshot pass caught it. After any edit to shader text, check that the shader draws and the fallback is hidden.pause, resume and renderStatic, for 2.5 KB gzipped. Kevin reversed it (2026-08-01): "add back the functions and whatever to configure, this is still good for making it into a library", and asked for the compiled output to be lighter at build time. Every parameter stays a uniform with a setter and a default, the build's minifier shrinks the shipped code, and any GLSL identifier minification runs as a build step over readable source.scripts/pixel-diff.mjs (capture <url> --out <png>, then compare <a.png> <b.png> --diff <png>, which exits 1 on any difference). The sequence is in references/recipes.md. Report residue exactly. The constant fold left 4 of the 4,039,200 channels in a 1,009,800-pixel capture off by 1/255 each, because GPU drivers may fuse multiply-adds differently after any source change. The revert diffed at zero.Agent that global fetch lacks; each kept package carries a one-line comment saying why. The contact route moved onto the shared limiter in gt-cloud's apps/landing/src/lib/rate-limit.ts.pnpm build && pnpm start and point npx -y lighthouse@12 at port 3000 (the full command is in references/recipes.md). For generaltranslation.com, gt-website references/docs.md ("Performance") holds the local command, the cache check per framework and locale, and the way into SSO-protected previews.gh pr comment inside a pipeline failed silently because the command takes no --jq flag, and the round's numbers never posted.A prerendered page never bakes a live value into its HTML. On 2026-08-18 the GitHub star badge on a blog post was missing in production while localhost showed it: the prerender's GitHub fetch had failed, and the HTML served the empty result. Kevin: "maybe it shouldnt be a server side. we can cache on server side". The pattern on gt-cloud main (#4380):
formatGitHubStarCountKnownMinimum in apps/landing/src/lib/github-stars.ts) as its first label.apps/landing/src/app/api/github-stars/route.ts is a same-origin route. getGitHubStarCount() (apps/landing/src/lib/github-stars.server.ts) fetches GitHub with next: { revalidate: 3600 }, a 5 s timeout and a schema check. The route answers with Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400, or with 503 and no-store when GitHub fails.apps/landing/src/components/ui/GitHubStarCount.tsx fetches the route once on mount through useMountEffect, aborts on unmount and ignores values under the known minimum. It holds its slot, invisible while a deferred label resolves, so the badge never shifts the layout.Any count, status or price that changes after the build uses the same shape.
BUDGET block: median triangles per frame at 1080p, every shadow pass included, over a fixed 70 s scripted run, capped at 7 million. Two content rounds raised the median to between 10 and 12 million, and the gate failed hard. The regression note named the rounds, the medians before and after, the mechanism and the fix.--max-median, --max-slow, --max-contexts and --max-long and exits 1 when the median run breaches one. Run it from Prototemplate against the gt-cloud page, and put the command and its output in the pull request body. Probe scripts stay out of the gt-cloud diff (gt-ship section 3, "Verification instruments stay out"). A red budget blocks the change like any other failing check.willReadFrequently canvas.#version comes first; the library API is intact; each new dependency is justified on its own.In this set: gt-motion (the engine contract, the governor's place in it, the moving type law), gt-dither (the engines, their copies and their cost tables), gt-website (docs performance, Lighthouse commands, deploys), gt-verify (proving a visual fix), gt-local-dev (review servers and dev environments), gt-reporting (the before and after report), gt-ship (the pull request body, its screenshots and what stays out of the diff), gt-landing-pages (the contrast floors), gt-lints (gates). In gt-cloud: react-best-practices, react-useeffect, glyphfield, gt-landing. Wiki: perf (the cheap-to-invasive investigation ladder), core-web-vitals, agent-browser.
Dated provenance for every rule, script and number in this skill is in references/sources.md.
The folder skills/gt-performance as agents fetch it. Each file opens raw.
SKILL.mdreferences/recipes.mdreferences/sources.mdreferences/webgl-lifecycles.mdscripts/frame-probe.mjsscripts/pixel-diff.mjs