fix: isolate content layer from animated background to eliminate flickering

Root cause: backdrop-filter on header/nav resamples the animated
background 60fps, causing GPU compositing flicker (Chromium bug).

- Add isolation: isolate wrapper in AppShell to prevent backdrop-filter
  from seeing through to the animated background layer
- Add will-change: transform + translateZ(0) to BackgroundRenderer
  for stable GPU layer
- Replace contain: layout style with contain: content on main
- Replace transition-all with specific properties in Card component
This commit is contained in:
Fringg
2026-02-27 07:52:45 +03:00
parent d8cf4301ca
commit 04eacf6421
4 changed files with 142 additions and 136 deletions

View File

@@ -90,6 +90,8 @@ export function BackgroundRenderer() {
opacity: effectiveConfig.opacity, opacity: effectiveConfig.opacity,
filter: effectiveConfig.blur > 0 ? `blur(${effectiveConfig.blur}px)` : undefined, filter: effectiveConfig.blur > 0 ? `blur(${effectiveConfig.blur}px)` : undefined,
contain: 'strict', contain: 'strict',
willChange: 'transform',
transform: 'translateZ(0)',
}} }}
> >
<Suspense fallback={null}> <Suspense fallback={null}>

View File

@@ -11,7 +11,7 @@ const cardVariants = cva(
'relative overflow-hidden', 'relative overflow-hidden',
'border border-dark-700/40 bg-dark-900/70', 'border border-dark-700/40 bg-dark-900/70',
'rounded-[var(--bento-radius)]', 'rounded-[var(--bento-radius)]',
'transition-all duration-200', 'transition-[border-color,background-color,box-shadow,transform,opacity] duration-200',
// GPU acceleration // GPU acceleration
'transform-gpu', 'transform-gpu',
// Glass border inset // Glass border inset

View File

@@ -280,158 +280,162 @@ export function AppShell({ children }: AppShellProps) {
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
{/* Animated background */} {/* Animated background — on its own GPU layer, below everything */}
<BackgroundRenderer /> <BackgroundRenderer />
{/* Global components */} {/* Content layer — isolated from background to prevent backdrop-filter
<WebSocketNotifications /> from resampling the animated background every frame (Chromium bug) */}
<CampaignBonusNotifier /> <div className="relative z-10" style={{ isolation: 'isolate' }}>
<SuccessNotificationModal /> {/* Global components */}
<WebSocketNotifications />
<CampaignBonusNotifier />
<SuccessNotificationModal />
{/* Desktop Header */} {/* Desktop Header */}
<header className="fixed left-0 right-0 top-0 z-50 hidden border-b border-dark-800/50 bg-dark-950/80 backdrop-blur-xl lg:block"> <header className="fixed left-0 right-0 top-0 z-50 hidden border-b border-dark-800/50 bg-dark-950/80 backdrop-blur-xl lg:block">
<div className="mx-auto grid h-14 max-w-6xl grid-cols-[auto_1fr_auto] items-center gap-4 px-6"> <div className="mx-auto grid h-14 max-w-6xl grid-cols-[auto_1fr_auto] items-center gap-4 px-6">
{/* Logo */} {/* Logo */}
<Link to="/" className="flex items-center gap-2.5" onClick={handleNavClick}> <Link to="/" className="flex items-center gap-2.5" onClick={handleNavClick}>
<div className="relative flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg bg-dark-800"> <div className="relative flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg bg-dark-800">
<span <span
className={cn(
'absolute text-sm font-bold text-accent-400 transition-opacity duration-200',
hasCustomLogo && isLogoPreloaded() ? 'opacity-0' : 'opacity-100',
)}
>
{logoLetter}
</span>
{hasCustomLogo && logoUrl && (
<img
src={logoUrl}
alt={appName || 'Logo'}
className={cn( className={cn(
'absolute h-full w-full object-contain transition-opacity duration-200', 'absolute text-sm font-bold text-accent-400 transition-opacity duration-200',
isLogoPreloaded() ? 'opacity-100' : 'opacity-0', hasCustomLogo && isLogoPreloaded() ? 'opacity-0' : 'opacity-100',
)} )}
/> >
)} {logoLetter}
</div> </span>
<span className="text-base font-semibold text-dark-100">{appName}</span> {hasCustomLogo && logoUrl && (
</Link> <img
src={logoUrl}
alt={appName || 'Logo'}
className={cn(
'absolute h-full w-full object-contain transition-opacity duration-200',
isLogoPreloaded() ? 'opacity-100' : 'opacity-0',
)}
/>
)}
</div>
<span className="text-base font-semibold text-dark-100">{appName}</span>
</Link>
{/* Center Navigation */} {/* Center Navigation */}
<nav className="flex items-center justify-center gap-1"> <nav className="flex items-center justify-center gap-1">
{desktopNavItems.map((item) => ( {desktopNavItems.map((item) => (
<Link
key={item.path}
to={item.path}
onClick={handleNavClick}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive(item.path)
? 'bg-dark-800 text-dark-50'
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)}
>
<item.icon className="h-4 w-4" />
<span>{item.label}</span>
</Link>
))}
{referralEnabled && (
<Link
to="/referral"
onClick={handleNavClick}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('/referral')
? 'bg-dark-800 text-dark-50'
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)}
>
<UsersIcon className="h-4 w-4" />
<span>{t('nav.referral')}</span>
</Link>
)}
{isAdmin && (
<>
{/* Separator before admin */}
<div className="mx-2 h-5 w-px bg-dark-700" />
<Link <Link
to="/admin" key={item.path}
to={item.path}
onClick={handleNavClick} onClick={handleNavClick}
className={cn( className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors', 'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
location.pathname.startsWith('/admin') isActive(item.path)
? 'bg-warning-500/10 text-warning-400' ? 'bg-dark-800 text-dark-50'
: 'text-warning-500/70 hover:bg-warning-500/10 hover:text-warning-400', : 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)} )}
> >
<ShieldIcon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
<span>{t('admin.nav.title')}</span> <span>{item.label}</span>
</Link>
))}
{referralEnabled && (
<Link
to="/referral"
onClick={handleNavClick}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('/referral')
? 'bg-dark-800 text-dark-50'
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)}
>
<UsersIcon className="h-4 w-4" />
<span>{t('nav.referral')}</span>
</Link> </Link>
</>
)}
</nav>
{/* Right side actions */}
<div className="flex items-center justify-end gap-2">
<button
onClick={() => {
haptic.impact('light');
toggleTheme();
}}
className={cn(
'rounded-xl border border-dark-700/50 bg-dark-800/50 p-2 text-dark-400 transition-all duration-200 hover:bg-dark-700 hover:text-accent-400',
!canToggleTheme && 'pointer-events-none invisible',
)} )}
title={isDark ? t('theme.light') || 'Light mode' : t('theme.dark') || 'Dark mode'} {isAdmin && (
> <>
{isDark ? <MoonIcon className="h-5 w-5" /> : <SunIcon className="h-5 w-5" />} {/* Separator before admin */}
</button> <div className="mx-2 h-5 w-px bg-dark-700" />
<TicketNotificationBell isAdmin={location.pathname.startsWith('/admin')} /> <Link
<LanguageSwitcher /> to="/admin"
<button onClick={handleNavClick}
onClick={() => { className={cn(
haptic.impact('light'); 'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
logout(); location.pathname.startsWith('/admin')
}} ? 'bg-warning-500/10 text-warning-400'
className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-2 text-dark-400 transition-all duration-200 hover:bg-dark-700 hover:text-accent-400" : 'text-warning-500/70 hover:bg-warning-500/10 hover:text-warning-400',
title={t('nav.logout')} )}
> >
<LogoutIcon className="h-5 w-5" /> <ShieldIcon className="h-4 w-4" />
</button> <span>{t('admin.nav.title')}</span>
</Link>
</>
)}
</nav>
{/* Right side actions */}
<div className="flex items-center justify-end gap-2">
<button
onClick={() => {
haptic.impact('light');
toggleTheme();
}}
className={cn(
'rounded-xl border border-dark-700/50 bg-dark-800/50 p-2 text-dark-400 transition-all duration-200 hover:bg-dark-700 hover:text-accent-400',
!canToggleTheme && 'pointer-events-none invisible',
)}
title={isDark ? t('theme.light') || 'Light mode' : t('theme.dark') || 'Dark mode'}
>
{isDark ? <MoonIcon className="h-5 w-5" /> : <SunIcon className="h-5 w-5" />}
</button>
<TicketNotificationBell isAdmin={location.pathname.startsWith('/admin')} />
<LanguageSwitcher />
<button
onClick={() => {
haptic.impact('light');
logout();
}}
className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-2 text-dark-400 transition-all duration-200 hover:bg-dark-700 hover:text-accent-400"
title={t('nav.logout')}
>
<LogoutIcon className="h-5 w-5" />
</button>
</div>
</div> </div>
</div> </header>
</header>
{/* Mobile Header */} {/* Mobile Header */}
<AppHeader <AppHeader
mobileMenuOpen={mobileMenuOpen} mobileMenuOpen={mobileMenuOpen}
setMobileMenuOpen={setMobileMenuOpen} setMobileMenuOpen={setMobileMenuOpen}
onCommandPaletteOpen={() => {}} onCommandPaletteOpen={() => {}}
headerHeight={headerHeight} headerHeight={headerHeight}
isFullscreen={isMobileFullscreen} isFullscreen={isMobileFullscreen}
safeAreaInset={safeAreaInset} safeAreaInset={safeAreaInset}
contentSafeAreaInset={contentSafeAreaInset} contentSafeAreaInset={contentSafeAreaInset}
telegramPlatform={platform} telegramPlatform={platform}
wheelEnabled={wheelEnabled} wheelEnabled={wheelEnabled}
referralEnabled={referralEnabled} referralEnabled={referralEnabled}
hasContests={hasContests} hasContests={hasContests}
hasPolls={hasPolls} hasPolls={hasPolls}
/> />
{/* Desktop spacer */} {/* Desktop spacer */}
<div className="hidden h-14 lg:block" /> <div className="hidden h-14 lg:block" />
{/* Mobile spacer */} {/* Mobile spacer */}
<div className="lg:hidden" style={{ height: headerHeight }} /> <div className="lg:hidden" style={{ height: headerHeight }} />
{/* Main content */} {/* Main content */}
<main className="mx-auto max-w-6xl px-4 py-6 pb-28 lg:px-6 lg:pb-8">{children}</main> <main className="mx-auto max-w-6xl px-4 py-6 pb-28 lg:px-6 lg:pb-8">{children}</main>
{/* Mobile Bottom Navigation */} {/* Mobile Bottom Navigation */}
<MobileBottomNav <MobileBottomNav
isKeyboardOpen={isKeyboardOpen} isKeyboardOpen={isKeyboardOpen}
referralEnabled={referralEnabled} referralEnabled={referralEnabled}
wheelEnabled={wheelEnabled} wheelEnabled={wheelEnabled}
/> />
</div>
</div> </div>
); );
} }

View File

@@ -268,10 +268,10 @@
} }
} }
/* Optimize main scrollable content */ /* Main content — containment removed to avoid GPU layer conflicts
with isolation: isolate on content wrapper */
main { main {
/* Prevent layout shifts during scroll */ contain: content;
contain: layout style;
} }
/* Light theme - Champagne */ /* Light theme - Champagne */