How General Translation keeps visual surfaces fast with no change to how they look.
node scripts/install-skills.mjs 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.getContext returns null and fields draw black with no error. On 2026-07-29 a direction's shader rendered on a fresh load and was black after 12 client-side switches between directions.src/lib/studio-field.ts, src/lib/prismatic-field.ts and src/lib/horizon-field.ts each draw into one offscreen GL canvas, blit into every subscriber's 2D canvas from one rAF loop, and keep the context when a subscriber's destroy() runs, so it survives route switches. gt-dither section 2 and references/engines.md hold the mechanism and the rebuild after a lost context.getImageData stays off the blit path. willReadFrequently pins a canvas to CPU memory, so every drawImage from it uploads to the GPU. That upload was the glyph rain's mobile lag, worst on WebKit. The glyph field snapshots each finished atlas into an ImageBitmap and blits from the snapshot (atlasSrc in src/lib/glyph-field.ts).gl.getExtension('WEBGL_lose_context')?.loseContext(). gt-cloud's apps/landing/src/components/landing/shell/V0FooterMark.tsx does this after a pull request review caught the footer mark leaking one context per mount (2026-08-08). The same file adds its GSAP ticker callback only while the mark is on screen. A registered callback that returns early still keeps GSAP's global rAF loop awake site-wide.useMountEffect (packages/ui/src/hooks/use-mount-effect.ts) is a mount-only useEffect, so React strict mode runs setup, cleanup and setup again in dev. Prototemplate's src/lib/use-mount-effect.ts defers cleanup by one task so the simulated rerun cancels it. The headline engine's cleanup removes the dust canvas and the two guides it appended (EverySentence.tsx, and gt-cloud's HomeHero.tsx). Probing on 2026-08-07 found the dev double effect leaving a dead duplicate canvas and guide pair in the DOM, and the cleanup landed the same day. Check in dev that exactly one canvas and one of each created node exist after mount and after a hot reload.getContext on the page's canvases creates contexts and exhausts the budget it measures (2026-07-29). Screenshot after real navigation, or wrap getContext before page scripts run, as frame-probe does (its gl column).destroy() (DESIGN.md section 11, gt-motion section 6).src/app/layout.tsx) installs a rAF gate in every page: a parent posts { type: 'gt:freeze', frozen }, callbacks queue while frozen, and the queue flushes on resume so loops continue where they stopped. src/app/present/viewer/LazyFrame.tsx mounts an iframe only near the viewport (far frames release their contexts), freezes it 2.8 s after load and lets it animate under the pointer. Gallery tiles show static captures (public/shots/<theme>/<slug>.jpg), and src/app/directions/DirectionFrame.tsx keeps the capture behind its one live frame until the frame loads. The gate wraps requestAnimationFrame only, so a new scene animates on rAF; a loop on setInterval or setTimeout keeps running inside a frozen preview.packages/ui/src/lib/ (glyph-field.ts, dither.ts, picture-field.ts) and the apps mount them (for example apps/landing/src/components/landing/shared/GlyphRain.tsx). Browser zoom changes devicePixelRatio without resizing the box, so a ResizeObserver alone leaves the blits soft. gt-cloud's glyph field coalesces ResizeObserver callbacks into one rAF, watches matchMedia('(resolution: <dpr>dppx)') and re-arms it on every change, keeps resize() idempotent for when both fire, and remaps in-flight positions by the box ratio.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.
src/lib/glyph-field.ts (the governor constants, govern(), start(), the prepared sample set, the atlas snapshot, ditherAtlasRows, TIER_COVER), src/app/d/production/sections/ink-field.ts (the capped loop's governor), src/lib/studio-field.ts, src/lib/prismatic-field.ts and src/lib/horizon-field.ts (the shared context, SHADER NOTES, #version), src/lib/dither.ts, src/lib/use-mount-effect.ts, src/components/shared/EverySentence.tsx (cleanup), src/app/layout.tsx (the rAF gate), src/app/present/viewer/LazyFrame.tsx, src/app/directions/DirectionFrame.tsx, DESIGN.md section 11, docs/LIBRARIES.md, package.json, .next/static/chunks of the 2026-10-01 build.gt-motion sections 4 and 6, gt-dither section 2 and references/engines.md, gt-website section 8 and references/docs.md, gt-landing-pages ("Process"), gt-ship sections 3 and 4, gt-aesthetic/scripts/measure-type.mjs (the browser setup the scripts here reuse).apps/landing/src/components/landing/shell/V0FooterMark.tsx, packages/ui/src/lib/glyph-field.ts, packages/ui/src/hooks/use-mount-effect.ts, apps/landing/src/components/landing/shared/GlyphRain.tsx, apps/landing/src/components/landing/home/sections/HomeHero.tsx, apps/landing/src/components/landing/home/sections/translate-window.css, apps/landing/src/app/api/github-stars/route.ts, apps/landing/src/lib/github-stars.ts, apps/landing/src/lib/github-stars.server.ts, apps/landing/src/components/ui/GitHubStarCount.tsx, apps/landing/src/lib/rate-limit.ts, apps/landing/src/components/landing/shared/lang.css, apps/landing/package.json, apps/landing/vercel.json; pull requests #4380 and #4815.docs-perf-investigation, lighthouse-round-conventions, redesign-screenshot-harness, pr-size-discipline, and the 2026-09-26 ship-lessons note on shared-machine load.The folder skills/gt-performance as agents fetch it. Each file opens raw.
SKILL.mdreferences/recipes.mdscripts/frame-probe.mjsscripts/pixel-diff.mjs