Files
openchamber/scripts/perf/animation-fixture.html
T
Bohdan Triapitsyn ca31157584 perf(tooling): add an animation cost profiler and document the harness
Adds `bun run profile:animation`: it serves an isolated fixture and measures
each animation variant directly, so comparing techniques takes seconds instead
of an application rebuild plus a streamed response.

The result is unambiguous and does not vary with element count, measured from 1
to 32: transform, opacity and filter cost zero extra style recalculations, while
the individual rotate property, background-position, border-color and box-shadow
each recalculate style 60 times a second, and geometry properties add layout on
top. Notably `rotate: 360deg` is not a cheap synonym for
`transform: rotate(360deg)`, and will-change, wrapper elements, containment and
stepped timing do not make a non-composited property cheap.

`scripts/perf/DOCUMENTATION.md` documents all four capture commands, how to
stand up a production build to measure against, how to read the artifacts, the
validity guarantees the scripts enforce, and the methodology rules, so this can
be handed to an agent as the entry point for measuring performance. It is linked
from the root guide's documentation anchors.

The theme skill gains an animation contract carrying the measured table, and the
performance skill points at the tooling documentation.
2026-08-03 18:38:58 +03:00

66 lines
3.5 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>OpenChamber animation cost fixture</title>
<style>
body { margin: 0; background: #111; color: #eee; font: 12px ui-monospace, monospace; }
.grid { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px; }
.cell { width: 24px; height: 24px; }
svg { width: 24px; height: 24px; display: block; }
.wrap { display: block; width: 24px; height: 24px; }
/*
One variant per animated property, so a run answers "what does animating
this cost" rather than "is this particular component slow". Compositor-
driven properties should cost a small constant; anything the compositor
cannot handle recalculates style every frame.
*/
@keyframes oc-transform-rotate { to { transform: rotate(360deg); } }
@keyframes oc-rotate-property { to { rotate: 360deg; } }
@keyframes oc-opacity { 0%, 100% { opacity: 0.2; } 50% { opacity: 1; } }
@keyframes oc-translate { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(6px); } }
@keyframes oc-scale { 0%, 100% { transform: scale(0.7); } 50% { transform: scale(1); } }
@keyframes oc-background-position { to { background-position: 200% 0; } }
@keyframes oc-border-color { 0%, 100% { border-color: #345; } 50% { border-color: #8ab; } }
@keyframes oc-box-shadow { 0%, 100% { box-shadow: 0 0 0 0 #8ab; } 50% { box-shadow: 0 0 8px 2px #8ab; } }
@keyframes oc-filter { 0%, 100% { filter: brightness(0.6); } 50% { filter: brightness(1.4); } }
@keyframes oc-width { 0%, 100% { width: 12px; } 50% { width: 24px; } }
.v-none svg { animation: none; }
.v-transform-rotate svg { animation: oc-transform-rotate 1s linear infinite; }
.v-transform-rotate-willchange svg { animation: oc-transform-rotate 1s linear infinite; will-change: transform; }
.v-transform-rotate-steps svg { animation: oc-transform-rotate 1s steps(12) infinite; }
.v-transform-rotate-wrapper .wrap { animation: oc-transform-rotate 1s linear infinite; }
.v-rotate-property svg { animation: oc-rotate-property 1s linear infinite; }
.v-opacity svg { animation: oc-opacity 1.2s ease-in-out infinite; }
.v-translate svg { animation: oc-translate 1.2s ease-in-out infinite; }
.v-scale svg { animation: oc-scale 1.2s ease-in-out infinite; }
.v-background-position .cell { background: linear-gradient(90deg, #223, #8ab, #223); background-size: 200% 100%; animation: oc-background-position 1.5s linear infinite; }
.v-border-color .cell { border: 2px solid #345; animation: oc-border-color 1.5s linear infinite; }
.v-box-shadow .cell { animation: oc-box-shadow 1.5s linear infinite; }
.v-filter svg { animation: oc-filter 1.5s linear infinite; }
.v-width svg { animation: oc-width 1.5s linear infinite; }
</style>
</head>
<body>
<div class="grid" id="grid"></div>
<script>
const SPINNER = '<svg viewBox="0 0 24 24" fill="none" stroke="#8ab" stroke-width="2">'
+ '<circle cx="12" cy="12" r="9" stroke-opacity=".25"/><path d="M21 12a9 9 0 0 0-9-9"/></svg>';
const params = new URLSearchParams(location.search);
const variant = params.get('variant') || 'none';
const count = Math.max(1, Number(params.get('count') || 2));
const grid = document.getElementById('grid');
grid.className = 'grid v-' + variant;
const usesWrapper = variant.endsWith('-wrapper');
for (let index = 0; index < count; index += 1) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.innerHTML = usesWrapper ? '<span class="wrap">' + SPINNER + '</span>' : SPINNER;
grid.appendChild(cell);
}
</script>
</body>
</html>