From 450dc56cbee5e444db7167c4195e34fbd1e53b41 Mon Sep 17 00:00:00 2001 From: hasithasandun Date: Thu, 2 Jul 2026 15:01:37 +0530 Subject: [PATCH] fix(www): resolve globe component memory leak and resizing issues (#46025) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix ## What is the current behavior? Resolves #46026 1. **Memory Leak**: The `` component in the `www` app creates a new `debouncedResize` event listener every time the theme changes, but in the `useEffect` cleanup function, it attempts to remove `onResize` instead. Because these are two different function references in memory, the listener is never removed, causing a memory leak that grows with theme toggling. 2. **Resolution Scaling**: The WebGL `cobe` globe instance is not notified of width changes dynamically. When a user resizes their browser window, the `` resizes via CSS, but the internal 3D rendering stretches and becomes distorted because `state.width` and `state.height` are not updated inside the `onRender` loop. 3. **Local Dev Rendering**: Due to async CSS loading in Next.js development environments, `offsetWidth` evaluates to 0 on initial mount, causing the globe to render invisibly until manually resized. ## What is the new behavior? 1. **Memory Leak Fixed**: Switched from a global `window.addEventListener('resize')` to a `ResizeObserver` attached directly to the canvas container, properly disconnecting it on unmount. 2. **Resolution Scaling Fixed**: Added `state.width` and `state.height` updates directly inside the `onRender` callback. The globe now maintains its aspect ratio and high resolution dynamically as the user resizes the browser. 3. **Local Dev Rendering Fixed**: The `ResizeObserver` correctly triggers a size recalculation exactly when the local Next.js CSS finishes injecting, allowing the globe to render immediately in local dev. 4. **Safe Timeout**: Wrapped the initial opacity update in a null check and correctly store/clear the `timeoutId` during unmount. ## Additional context ## Before https://github.com/user-attachments/assets/813e077d-bcf4-4584-b3da-46c01b6af001 https://github.com/user-attachments/assets/73cef575-27e3-44d2-a191-fd192dec3ef9 ## After https://github.com/user-attachments/assets/cd68c66d-6a35-43d0-971f-04627ef532fb ## Summary by CodeRabbit * **Bug Fixes** * Globe now adapts instantly to container size changes for smoother, consistent rendering. * Improved lifecycle cleanup to reliably stop background tasks and clear timers, reducing memory leaks. * Enhanced stability and performance for longer-running sessions of the Globe component. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46025?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- apps/www/components/Globe.tsx | 44 ++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/apps/www/components/Globe.tsx b/apps/www/components/Globe.tsx index 32308ef70d7..933080d908e 100644 --- a/apps/www/components/Globe.tsx +++ b/apps/www/components/Globe.tsx @@ -1,23 +1,27 @@ import createGlobe from 'cobe' -import { debounce } from 'lib/helpers' import { useTheme } from 'next-themes' -import { useCallback, useEffect, useRef } from 'react' +import { useEffect, useRef } from 'react' const Globe = () => { const { resolvedTheme } = useTheme() const canvasRef = useRef(null) - let rotation: number = 0 - let width: number = 0 - const onResize = useCallback( - () => canvasRef.current && (width = canvasRef.current.offsetWidth), - [resolvedTheme] - ) - useEffect(() => { - const debouncedResize = debounce(onResize, 10) - window.addEventListener('resize', debouncedResize) - onResize() + let rotation = 0 + let width = 0 + let previousWidth = 0 + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + width = Math.round(entry.contentRect.width) + } + }) + + if (canvasRef.current) { + resizeObserver.observe(canvasRef.current) + width = Math.round(canvasRef.current.getBoundingClientRect().width) + } + const cobe = createGlobe(canvasRef.current, { devicePixelRatio: 2, width: width * 2, @@ -52,11 +56,23 @@ const Globe = () => { onRender: (state) => { state.phi = rotation rotation += 0.0025 + if (width !== previousWidth) { + state.width = width * 2 + state.height = width * 2 + previousWidth = width + } }, }) - setTimeout(() => (canvasRef.current.style.opacity = '0.8'), 10) + + const timeoutId = setTimeout(() => { + if (canvasRef.current) { + canvasRef.current.style.opacity = '0.8' + } + }, 10) + return () => { - window.removeEventListener('resize', onResize) + resizeObserver.disconnect() + clearTimeout(timeoutId) cobe.destroy() } }, [resolvedTheme])