mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
perf(motion): convert layout-property transitions to transforms
Audit P1: 5 of 7 layout-property animations were transition-[width]
or transition-[left] — these trigger reflow + repaint every frame.
Convert to transform-based equivalents that run on the compositor.
Subscription.tsx — 3 progress bars + 1 toggle:
• Device usage bar (track w-16) — width % → scaleX with a 0.0625
floor (= 4px on 64px track) preserving the minWidth: 4px guard
when connectedDevices > 0.
• Traffic-purchase progress — absolute inset-0 + width % →
transform: scaleX with origin-left, gradient stretches with the
fill same as before.
• Subscription period progress — same conversion.
• Autopay toggle thumb — left: 3↔26px → translateX(0↔23px) with
fixed left: 3px. role='switch' + aria-checked unchanged.
globals.css — .progress-fill utility class:
• transition-property: width, background-color → transform,
background-color. Width set to w-full + origin-left so consumers
just set transform: scaleX(0..1). Currently has no .tsx consumers
(grep clean) but the API stays valid for future use.
Skipped (bounded one-shot, transform alternative has tradeoffs):
• Info.tsx + InfoPageView.tsx accordion height. Animating from 0
to content-driven scrollHeight needs layout one way or another —
grid-template-rows 0fr/1fr and max-height also layout-class
properties; transform: scaleY squishes text contents. Cost is
bounded: one animation per user click, not continuous, not on
critical path. Separate work if priority changes.
Visual parity: scaleX renders identical geometry to width % on the
inner fill. box-shadow on the device bar (0 0 8px 40)
scales proportionally — tiny visual difference at full width, none
visible at partial widths.
This commit is contained in:
@@ -997,13 +997,18 @@ export default function Subscription() {
|
||||
className="h-[6px] w-full overflow-hidden rounded-full"
|
||||
style={{ background: g.textGhost }}
|
||||
>
|
||||
{/* scaleX (compositor) instead of width (layout-thrash).
|
||||
Track is 64px (w-16), so 0.0625 floor = 4px minimum,
|
||||
preserving the prior minWidth behaviour. */}
|
||||
<div
|
||||
className="h-full rounded-full transition-[width] duration-500"
|
||||
className="h-full w-full origin-left rounded-full transition-transform duration-500"
|
||||
style={{
|
||||
width: `${Math.round((connectedDevices / subscription.device_limit) * 100)}%`,
|
||||
transform: `scaleX(${(() => {
|
||||
const pct = connectedDevices / subscription.device_limit;
|
||||
return connectedDevices > 0 ? Math.max(pct, 0.0625) : 0;
|
||||
})()})`,
|
||||
background: zone.mainHex,
|
||||
boxShadow: `0 0 8px ${zone.mainHex}40`,
|
||||
minWidth: connectedDevices > 0 ? '4px' : '0px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -1146,9 +1151,9 @@ export default function Subscription() {
|
||||
style={{ background: g.trackBg }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 rounded-full transition-[width] duration-500"
|
||||
className="absolute inset-0 origin-left rounded-full transition-transform duration-500"
|
||||
style={{
|
||||
width: `${purchase.progress_percent}%`,
|
||||
transform: `scaleX(${purchase.progress_percent / 100})`,
|
||||
background: `linear-gradient(90deg, ${zone.mainHex}, ${zone.mainHex}80)`,
|
||||
}}
|
||||
/>
|
||||
@@ -1193,10 +1198,15 @@ export default function Subscription() {
|
||||
background: subscription.autopay_enabled ? zone.mainHex : g.textGhost,
|
||||
}}
|
||||
>
|
||||
{/* translateX (compositor) instead of left (layout-thrash).
|
||||
Resting position pinned at left:3px; on toggles a 23px
|
||||
slide on the GPU. */}
|
||||
<span
|
||||
className="absolute top-[3px] h-[22px] w-[22px] rounded-full bg-white transition-[left] duration-300"
|
||||
className="absolute left-[3px] top-[3px] h-[22px] w-[22px] rounded-full bg-white transition-transform duration-300"
|
||||
style={{
|
||||
left: subscription.autopay_enabled ? '26px' : '3px',
|
||||
transform: subscription.autopay_enabled
|
||||
? 'translateX(23px)'
|
||||
: 'translateX(0)',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
/>
|
||||
@@ -1387,9 +1397,9 @@ export default function Subscription() {
|
||||
style={{ background: g.trackBg }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 rounded-full transition-[width] duration-500"
|
||||
className="absolute inset-0 origin-left rounded-full transition-transform duration-500"
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
transform: `scaleX(${progress / 100})`,
|
||||
background:
|
||||
'linear-gradient(90deg, rgb(var(--color-accent-500)), rgb(var(--color-accent-400)))',
|
||||
}}
|
||||
|
||||
@@ -630,8 +630,10 @@ img.twemoji {
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
@apply h-full rounded-full duration-500 ease-smooth;
|
||||
transition-property: width, background-color;
|
||||
/* GPU-composited progress fill — consumer sets transform: scaleX(0..1)
|
||||
with transform-origin: left. Avoids width-triggered reflow per frame. */
|
||||
@apply h-full w-full origin-left rounded-full duration-500 ease-smooth;
|
||||
transition-property: transform, background-color;
|
||||
}
|
||||
|
||||
/* Stat card - Dark */
|
||||
|
||||
Reference in New Issue
Block a user