Skip to content

Optimize the NavBar Scaling using will-change-transform #491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 33 additions & 24 deletions template/app/src/client/components/NavBar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useAuth } from 'wasp/client/auth';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../../components/ui/sheet';
import { cn } from '../../../lib/utils';
import { throttleWithTrailingInvocation } from '../../../shared/utils';
import { UserDropdown } from '../../../user/UserDropdown';
import { UserMenuItems } from '../../../user/UserMenuItems';
import { useIsLandingPage } from '../../hooks/useIsLandingPage';
Expand All @@ -23,22 +22,26 @@ export default function NavBar({ navigationItems }: { navigationItems: Navigatio
const isLandingPage = useIsLandingPage();

useEffect(() => {
const throttledHandler = throttleWithTrailingInvocation(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 0);
}, 50);
};

window.addEventListener('scroll', throttledHandler);
window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', throttledHandler);
throttledHandler.cancel();
window.removeEventListener('scroll', handleScroll);
};
}, []);

return (
<>
{isLandingPage && <Announcement />}
<header className={cn('sticky top-0 z-50 transition-all duration-300', isScrolled && 'top-4')}>
<header
className={cn(
'sticky top-0 z-50 transition-transform duration-300 ease-out will-change-transform',
isScrolled && 'translate-y-4'
)}
>
<div
className={cn('transition-all duration-300', {
'mx-4 md:mx-20 pr-2 lg:pr-0 rounded-full shadow-lg bg-background/90 backdrop-blur-lg border border-border':
Expand All @@ -60,10 +63,13 @@ export default function NavBar({ navigationItems }: { navigationItems: Navigatio
>
<NavLogo isScrolled={isScrolled} />
<span
className={cn('font-semibold leading-6 text-foreground transition-all duration-300', {
'ml-2 text-sm': !isScrolled,
'ml-2 text-xs': isScrolled,
})}
className={cn(
'font-semibold leading-6 text-foreground transition-transform duration-300 ease-out will-change-transform',
{
'ml-2 text-sm transform scale-100': !isScrolled,
'ml-2 text-sm transform scale-90': isScrolled,
}
)}
>
Your SaaS
</span>
Expand Down Expand Up @@ -93,18 +99,21 @@ function NavBarDesktopUserDropdown({ isScrolled }: { isScrolled: boolean }) {
{isUserLoading ? null : !user ? (
<WaspRouterLink
to={routes.LoginRoute.to}
className={cn('font-semibold leading-6 ml-3 transition-all duration-300', {
'text-sm': !isScrolled,
'text-xs': isScrolled,
})}
className={cn(
'font-semibold leading-6 ml-3 transition-transform duration-300 ease-out will-change-transform',
{
'text-sm transform scale-100': !isScrolled,
'text-sm transform scale-90': isScrolled,
}
)}
>
<div className='flex items-center duration-300 ease-in-out text-foreground hover:text-primary transition-colors'>
Log in{' '}
<LogIn
size={isScrolled ? '1rem' : '1.1rem'}
className={cn('transition-all duration-300', {
'ml-1 mt-[0.1rem]': !isScrolled,
'ml-1': isScrolled,
className={cn('transition-transform duration-300 ease-out will-change-transform', {
'ml-1 mt-[0.1rem] transform scale-100': !isScrolled,
'ml-1 transform scale-90': isScrolled,
})}
/>
</div>
Expand Down Expand Up @@ -140,9 +149,9 @@ function NavBarMobileMenu({
>
<span className='sr-only'>Open main menu</span>
<Menu
className={cn('transition-all duration-300', {
'size-8 p-1': !isScrolled,
'size-6 p-0.5': isScrolled,
className={cn('transition-transform duration-300 ease-out will-change-transform', {
'size-8 p-1 transform scale-100': !isScrolled,
'size-8 p-1 transform scale-75': isScrolled,
})}
aria-hidden='true'
/>
Expand Down Expand Up @@ -213,9 +222,9 @@ function renderNavigationItems(

const NavLogo = ({ isScrolled }: { isScrolled: boolean }) => (
<img
className={cn('transition-all duration-500', {
'size-8': !isScrolled,
'size-7': isScrolled,
className={cn('transition-transform duration-300 ease-out will-change-transform', {
'size-8 transform scale-100': !isScrolled,
'size-8 transform scale-90': isScrolled,
})}
src={logo}
alt='Your SaaS App'
Expand Down
49 changes: 0 additions & 49 deletions template/app/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,3 @@
export function assertUnreachable(x: never): never {
throw Error('This code should be unreachable');
}

/**
* Allows for throttling a function call while still allowing the last invocation to be executed after the throttle delay ends.
*/
export function throttleWithTrailingInvocation(
fn: () => void,
delayInMilliseconds: number
): ((...args: any[]) => void) & { cancel: () => void } {
let fnLastCallTime: number | null = null;
let trailingInvocationTimeoutId: ReturnType<typeof setTimeout> | null = null;
let isTrailingInvocationPending = false;

const callFn = () => {
fnLastCallTime = Date.now();
fn();
};

const throttledFn = () => {
const currentTime = Date.now();
const timeSinceLastExecution = fnLastCallTime ? currentTime - fnLastCallTime : 0;

const shouldCallImmediately = fnLastCallTime === null || timeSinceLastExecution >= delayInMilliseconds;

if (shouldCallImmediately) {
callFn();
return;
}

if (!isTrailingInvocationPending) {
isTrailingInvocationPending = true;
const remainingDelayTime = Math.max(delayInMilliseconds - timeSinceLastExecution, 0);

trailingInvocationTimeoutId = setTimeout(() => {
callFn();
isTrailingInvocationPending = false;
}, remainingDelayTime);
}
};

throttledFn.cancel = () => {
if (trailingInvocationTimeoutId) {
clearTimeout(trailingInvocationTimeoutId);
trailingInvocationTimeoutId = null;
}
isTrailingInvocationPending = false;
};

return throttledFn as typeof throttledFn & { cancel: () => void };
}
Loading