From 12c97a2c5ebf0d3dc776f581589a9d4280fbdc2e Mon Sep 17 00:00:00 2001 From: Fringg Date: Fri, 27 Feb 2026 08:14:20 +0300 Subject: [PATCH] fix: render animated background via portal at z-index:-1 to stop implicit compositing Previous isolation:isolate fix didn't work because: - position:fixed elements still see through isolation boundary - backdrop-filter samples all rendered content regardless of stacking context - The real issue: animated background at z-index:0 with will-change:transform forces Chrome to implicitly composite EVERY overlapping element Fix: render BackgroundRenderer via createPortal on document.body with z-index:-1, placing it below the root stacking context. This eliminates implicit compositing entirely. Also: - Remove backdrop-blur-xl from desktop header (was resampling bg 60fps) - Remove will-change:opacity from card ::after pseudo-element - Replace transition-all with transition-colors on header buttons --- .../backgrounds/BackgroundRenderer.tsx | 16 +- src/components/layout/AppShell/AppShell.tsx | 282 +++++++++--------- src/styles/globals.css | 1 - 3 files changed, 150 insertions(+), 149 deletions(-) diff --git a/src/components/backgrounds/BackgroundRenderer.tsx b/src/components/backgrounds/BackgroundRenderer.tsx index b802cf3..d9dcb27 100644 --- a/src/components/backgrounds/BackgroundRenderer.tsx +++ b/src/components/backgrounds/BackgroundRenderer.tsx @@ -1,4 +1,5 @@ import { Suspense, useMemo } from 'react'; +import { createPortal } from 'react-dom'; import { useQuery } from '@tanstack/react-query'; import { brandingApi } from '@/api/branding'; import type { AnimationConfig, BackgroundType } from '@/components/ui/backgrounds/types'; @@ -83,20 +84,25 @@ export function BackgroundRenderer() { ? reduceMobileSettings(effectiveConfig.settings) : effectiveConfig.settings; - return ( + // Render via portal on document.body with z-index: -1. + // This places the animated background BELOW the root stacking context, + // preventing Chrome's implicit compositing from promoting every + // overlapping element to its own GPU layer (the root cause of flickering). + return createPortal(
0 ? `blur(${effectiveConfig.blur}px)` : undefined, contain: 'strict', - willChange: 'transform', - transform: 'translateZ(0)', + backfaceVisibility: 'hidden', }} > -
+ , + document.body, ); } diff --git a/src/components/layout/AppShell/AppShell.tsx b/src/components/layout/AppShell/AppShell.tsx index bd9a2a6..4752ed0 100644 --- a/src/components/layout/AppShell/AppShell.tsx +++ b/src/components/layout/AppShell/AppShell.tsx @@ -280,162 +280,158 @@ export function AppShell({ children }: AppShellProps) { return (
- {/* Animated background — on its own GPU layer, below everything */} + {/* Animated background renders via portal on document.body at z-index: -1 */} - {/* Content layer — isolated from background to prevent backdrop-filter - from resampling the animated background every frame (Chromium bug) */} -
- {/* Global components */} - - - + {/* Global components */} + + + - {/* Desktop Header */} -
-
- {/* Logo */} - -
- - {logoLetter} - - {hasCustomLogo && logoUrl && ( - {appName - )} -
- {appName} - - - {/* Center Navigation */} - - - {/* Right side actions */} -
- - - - + {logoLetter} + + {hasCustomLogo && logoUrl && ( + {appName + )}
+ {appName} + + + {/* Center Navigation */} + + + {/* Right side actions */} +
+ + + +
-
+
+ - {/* Mobile Header */} - {}} - headerHeight={headerHeight} - isFullscreen={isMobileFullscreen} - safeAreaInset={safeAreaInset} - contentSafeAreaInset={contentSafeAreaInset} - telegramPlatform={platform} - wheelEnabled={wheelEnabled} - referralEnabled={referralEnabled} - hasContests={hasContests} - hasPolls={hasPolls} - /> + {/* Mobile Header */} + {}} + headerHeight={headerHeight} + isFullscreen={isMobileFullscreen} + safeAreaInset={safeAreaInset} + contentSafeAreaInset={contentSafeAreaInset} + telegramPlatform={platform} + wheelEnabled={wheelEnabled} + referralEnabled={referralEnabled} + hasContests={hasContests} + hasPolls={hasPolls} + /> - {/* Desktop spacer */} -
+ {/* Desktop spacer */} +
- {/* Mobile spacer */} -
+ {/* Mobile spacer */} +
- {/* Main content */} -
{children}
+ {/* Main content */} +
{children}
- {/* Mobile Bottom Navigation */} - -
+ {/* Mobile Bottom Navigation */} +
); } diff --git a/src/styles/globals.css b/src/styles/globals.css index b61e32b..5746cf1 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -400,7 +400,6 @@ img.twemoji { transition: opacity 0.3s ease; pointer-events: none; z-index: 10; - will-change: opacity; } .bento-card-hover:hover::after {