mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
perf(dashboard): place live EPG progress fills without layout or animation (#1721)
* perf(dashboard): slide live EPG progress fills with a compositor transform The live-programme bars on channel rail cards and the hero animated their width over 0.4 s whenever the 30 s live-EPG tick moved them. Width is a layout property, so each tick re-laid out the whole document for about 24 frames, also while the window was minimized. Slide a full-width fill with translateX driven by a --live-progress custom property instead; Chromium runs that transition on the compositor. Reduced motion drops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(dashboard): format the live progress spec * perf(dashboard): stop animating the live EPG progress fills A live-EPG tick moves the bar by under one percent, so the 0.4 s transform transition was invisible but still produced a burst of compositor frames on every tick. Place the fill without a transition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(dashboard): pin the hero progress fill to its custom property The hero moved into DashboardHeroComponent (#1738); its progress bar gets the same transform fill as the rail cards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
7 files changed
+63
-4
No files matched your search
@@ -0,0 +1,8 @@
|
||||
---
|
||||
type: perf
|
||||
area: dashboard
|
||||
---
|
||||
|
||||
Live programme progress bars on the dashboard now move without animating or
|
||||
redrawing the whole page, so a dashboard left open with recent live channels
|
||||
uses less CPU and GPU.
|
||||
@@ -141,7 +141,7 @@
|
||||
aria-valuemax="100"
|
||||
[attr.aria-valuenow]="slide.progress"
|
||||
>
|
||||
<i [style.width.%]="slide.progress"></i>
|
||||
<i [style.--hero-progress]="slide.progress"></i>
|
||||
</span>
|
||||
}
|
||||
|
||||
|
||||
@@ -339,10 +339,15 @@
|
||||
|
||||
i {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--app-selection-color, #78adff);
|
||||
transition: width 0.4s ease;
|
||||
// A full-width fill slid into place, neither a width nor animated: a
|
||||
// live-EPG tick moves it by under one percent, so an animation was
|
||||
// invisible yet cost a layout pass (width) or a burst of compositor
|
||||
// frames (transform transition) on every tick.
|
||||
transform: translateX(calc(var(--hero-progress, 0) * 1% - 100%));
|
||||
}
|
||||
|
||||
&--live i {
|
||||
|
||||
@@ -254,6 +254,19 @@ describe('DashboardHeroComponent', () => {
|
||||
).toBe('/workspace/a');
|
||||
});
|
||||
|
||||
it('positions the progress fill through a custom property, never its width', () => {
|
||||
// Animating width re-lays out the page on every live-EPG tick; the
|
||||
// stylesheet slides the fill with a transform driven by this property.
|
||||
slides.set([
|
||||
{ ...slide('a', 'Live'), contentType: 'live', progress: 42 },
|
||||
]);
|
||||
render();
|
||||
|
||||
const fill = host().querySelector<HTMLElement>('.hero__progress i');
|
||||
expect(fill?.style.getPropertyValue('--hero-progress')).toBe('42');
|
||||
expect(fill?.style.width).toBe('');
|
||||
});
|
||||
|
||||
it('renders nothing when there is nothing to feature', () => {
|
||||
slides.set([]);
|
||||
render();
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"
|
||||
>
|
||||
<i
|
||||
[style.width.%]="
|
||||
[style.--live-progress]="
|
||||
card.nowPlayingProgress ?? 0
|
||||
"
|
||||
></i>
|
||||
|
||||
@@ -727,10 +727,15 @@
|
||||
|
||||
i {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--app-live-color, #ff8f8f);
|
||||
border-radius: 2px;
|
||||
transition: width 0.4s ease;
|
||||
// A full-width fill slid into place, neither a width nor animated: a
|
||||
// live-EPG tick moves it by under one percent, so an animation was
|
||||
// invisible yet cost a layout pass (width) or a burst of compositor
|
||||
// frames (transform transition) on every tick.
|
||||
transform: translateX(calc(var(--live-progress, 0) * 1% - 100%));
|
||||
}
|
||||
|
||||
&--idle i {
|
||||
|
||||
@@ -335,6 +335,34 @@ describe('DashboardRailComponent', () => {
|
||||
expect(visible.at(-1)).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('positions the live progress fill through a custom property, never its width', async () => {
|
||||
// Animating width re-lays out the page for every frame of the
|
||||
// transition on each EPG tick; the stylesheet slides the fill with
|
||||
// a compositor transform driven by this property instead.
|
||||
installObservers(false);
|
||||
const { fixture } = await render([
|
||||
card({
|
||||
id: 'known',
|
||||
contentType: 'live',
|
||||
nowPlayingProgress: 37.5,
|
||||
}),
|
||||
card({ id: 'unknown', contentType: 'live' }),
|
||||
]);
|
||||
const fills = Array.from(
|
||||
(
|
||||
fixture.nativeElement as HTMLElement
|
||||
).querySelectorAll<HTMLElement>('.rail__channel-progress i')
|
||||
);
|
||||
expect(fills).toHaveLength(2);
|
||||
expect(fills[0].style.getPropertyValue('--live-progress')).toBe(
|
||||
'37.5'
|
||||
);
|
||||
expect(fills[1].style.getPropertyValue('--live-progress')).toBe(
|
||||
'0'
|
||||
);
|
||||
expect(fills.map((fill) => fill.style.width)).toEqual(['', '']);
|
||||
});
|
||||
|
||||
it('shows the placeholder only while a live card is pending its first answer', async () => {
|
||||
installObservers(false);
|
||||
const { fixture } = await render([
|
||||
|
||||
Reference in new issue
Block a user