From c61e4730e924d4a707abd8cf6dba1e08062b8f0d Mon Sep 17 00:00:00 2001 From: Wojtek Majewski Date: Sat, 8 Nov 2025 14:31:45 +0100 Subject: [PATCH] perf: fix low fps on landing page --- pkgs/website/src/components/CodeOverlay.astro | 183 ++++-------------- pkgs/website/src/components/SlowScroll.astro | 58 ++++++ .../src/components/TestimonialCarousel.astro | 26 ++- pkgs/website/src/content/docs/index.mdx | 36 +--- pkgs/website/src/styles/global.css | 150 +++++++++----- 5 files changed, 229 insertions(+), 224 deletions(-) create mode 100644 pkgs/website/src/components/SlowScroll.astro diff --git a/pkgs/website/src/components/CodeOverlay.astro b/pkgs/website/src/components/CodeOverlay.astro index a3d565eb8..fa42d5174 100644 --- a/pkgs/website/src/components/CodeOverlay.astro +++ b/pkgs/website/src/components/CodeOverlay.astro @@ -37,7 +37,7 @@ align-items: center; justify-content: center; z-index: 5; - transition: opacity 0.3s ease; + transition: opacity 0.2s ease; } .code-overlay.hidden { @@ -55,13 +55,9 @@ font-weight: 900; color: var(--sl-color-text); margin-bottom: 1.5rem; - text-shadow: 0 12px 60px var(--sl-color-bg), - 0 10px 50px var(--sl-color-bg), - 0 8px 40px var(--sl-color-bg), - 0 6px 30px var(--sl-color-bg), - 0 4px 20px var(--sl-color-bg), - 0 2px 12px var(--sl-color-bg), - 0 0 80px rgba(0, 123, 110, 1); + /* Simplified text-shadow for better performance */ + text-shadow: 0 2px 20px rgba(0, 123, 110, 0.8), + 0 4px 40px var(--sl-color-bg); } .overlay-list { @@ -71,13 +67,9 @@ margin-bottom: 2rem; text-align: left; font-weight: 600; - text-shadow: 0 12px 60px var(--sl-color-bg), - 0 10px 50px var(--sl-color-bg), - 0 8px 40px var(--sl-color-bg), - 0 6px 30px var(--sl-color-bg), - 0 4px 20px var(--sl-color-bg), - 0 2px 12px var(--sl-color-bg), - 0 0 80px rgba(0, 123, 110, 1); + /* Simplified text-shadow for better performance */ + text-shadow: 0 2px 20px rgba(0, 123, 110, 0.8), + 0 4px 40px var(--sl-color-bg); } /* Base button styling following Starlight's pattern */ @@ -97,28 +89,16 @@ padding: 0.9375rem 1.25rem; text-decoration: none; - /* Glow and animation effects */ + /* Static glow effect - no continuous animation for better performance */ box-shadow: 0 8px 32px rgba(0, 123, 110, 0.4), - 0 0 60px rgba(0, 123, 110, 0.25); - animation: pulse-glow 2s ease-in-out infinite; + 0 0 40px rgba(0, 123, 110, 0.2); position: relative; overflow: hidden; pointer-events: auto; /* Smooth transitions */ - transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1); - } - - @keyframes pulse-glow { - 0%, 100% { - box-shadow: 0 8px 32px rgba(0, 123, 110, 0.4), - 0 0 60px rgba(0, 123, 110, 0.25); - } - 50% { - box-shadow: 0 8px 40px rgba(0, 123, 110, 0.55), - 0 0 80px rgba(0, 123, 110, 0.4); - } + transition: transform 0.3s ease, + box-shadow 0.3s ease; } .reveal-button::before { @@ -137,10 +117,9 @@ .reveal-button.sl-button-primary:hover { color: var(--sl-color-black); - transform: scale(1.08); - box-shadow: 0 8px 32px rgba(0, 123, 110, 0.4), - 0 0 60px rgba(0, 123, 110, 0.25); - animation: none; + transform: scale(1.05); + box-shadow: 0 8px 40px rgba(0, 123, 110, 0.5), + 0 0 50px rgba(0, 123, 110, 0.3); } .reveal-button:hover::before { @@ -251,8 +230,8 @@ .reveal-button::before, .reset-overlay-button, .scroll-top-button { - transition: none; - animation: none; + transition: none !important; + animation: none !important; } } @@ -275,7 +254,6 @@ min-height: 0; overflow: auto; /* The actual scroller */ padding: 0; - scroll-behavior: smooth; scrollbar-width: thin; scrollbar-color: var(--sl-color-accent) var(--sl-color-gray-5); } @@ -324,10 +302,7 @@ } } - // Global flag to track if auto-scroll animation is running - let isAutoScrolling = false; - - // Flag to track if programmatic scroll is happening (auto-scroll or scroll-to-top) + // Flag to track if programmatic scroll is happening (scroll-to-top button) let isProgrammaticScroll = false; // Flag to track if manual scroll event has been sent (only send once) @@ -341,111 +316,45 @@ if (!button || !overlay || !scrollableContainer || !scrollableInner) return; - button.addEventListener('click', async () => { + button.addEventListener('click', () => { // Track custom event in Plausible if (typeof window.plausible !== 'undefined') { window.plausible('home:reveal-code'); } - const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - - // Helper to animate scroll with custom duration (cancellable on user interaction) - const animateScroll = (element: HTMLElement, targetScroll: number, duration: number) => { - return new Promise((resolve) => { - const start = element.scrollTop; - const distance = targetScroll - start; - const startTime = performance.now(); - let animationId: number | null = null; - let cancelled = false; - - // Cancel animation if user manually scrolls - const cancelAnimation = () => { - cancelled = true; - isAutoScrolling = false; - if (animationId !== null) { - cancelAnimationFrame(animationId); - } - cleanup(); - resolve(); - }; - - // Listen for user scroll interactions - const handleWheel = () => cancelAnimation(); - const handleTouch = () => cancelAnimation(); - const handleClick = () => cancelAnimation(); - const handleKeydown = (e: KeyboardEvent) => { - if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '].includes(e.key)) { - cancelAnimation(); - } - }; - - element.addEventListener('wheel', handleWheel, { passive: true }); - element.addEventListener('touchstart', handleTouch, { passive: true }); - element.addEventListener('click', handleClick); - element.addEventListener('keydown', handleKeydown); - - const cleanup = () => { - element.removeEventListener('wheel', handleWheel); - element.removeEventListener('touchstart', handleTouch); - element.removeEventListener('click', handleClick); - element.removeEventListener('keydown', handleKeydown); - }; - - const scroll = (currentTime: number) => { - if (cancelled) return; - - const elapsed = currentTime - startTime; - const progress = Math.min(elapsed / duration, 1); - - // Ease-in-out quadratic for gentle smoothing - const eased = progress < 0.5 - ? 2 * progress * progress - : 1 - Math.pow(-2 * progress + 2, 2) / 2; - - element.scrollTop = start + distance * eased; - - if (progress < 1) { - animationId = requestAnimationFrame(scroll); - } else { - cleanup(); - isAutoScrolling = false; - resolve(); - } - }; - - animationId = requestAnimationFrame(scroll); - }); - }; - - // 1. Enable scrolling and start animation first - isAutoScrolling = true; scrollableContainer.classList.add('scrollable-enabled'); - const scrollPromise = animateScroll(scrollableInner, scrollableInner.scrollHeight, 3000); + overlay.style.opacity = '0'; - // Small delay so scroll starts accelerating before overlay fades - await wait(100); + // Custom 3-second scroll animation + const startScroll = scrollableInner.scrollTop; + const targetScroll = scrollableInner.scrollHeight; + const distance = targetScroll - startScroll; + const duration = 3000; // 3 seconds + const startTime = performance.now(); - // 2. Fade out overlay while scroll is already in motion - overlay.style.opacity = '0'; + function easeOutCubic(t: number) { + return 1 - Math.pow(1 - t, 3); + } - // Wait for overlay fade, then hide it - await wait(300); - overlay.classList.add('hidden'); + function animateScroll(currentTime: number) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + const easedProgress = easeOutCubic(progress); - // 3. Wait for scroll to complete - await scrollPromise; + scrollableInner.scrollTop = startScroll + (distance * easedProgress); - // 4. Show reset button after scroll completes - const resetButton = document.querySelector('.reset-overlay-button') as HTMLElement; - if (resetButton) { - resetButton.style.display = 'block'; + if (progress < 1) { + requestAnimationFrame(animateScroll); + } } - // 5. Trigger scroll-to-top button visibility check now that animation is done - const scrollTopButton = document.querySelector('.scroll-top-button') as HTMLElement; - if (scrollTopButton && scrollableInner.scrollTop > 50) { - scrollTopButton.style.display = 'block'; - } + requestAnimationFrame(animateScroll); + + setTimeout(() => overlay.classList.add('hidden'), 200); + setTimeout(() => { + const resetButton = document.querySelector('.reset-overlay-button') as HTMLElement; + if (resetButton) resetButton.style.display = 'block'; + }, 2000); }); }; @@ -457,12 +366,6 @@ // Show/hide button based on scroll position const updateButtonVisibility = () => { - // Don't show button during auto-scroll animation - if (isAutoScrolling) { - button.style.display = 'none'; - return; - } - // Track manual scroll event (only once, and only if not programmatic) if (!hasTrackedManualScroll && !isProgrammaticScroll) { hasTrackedManualScroll = true; diff --git a/pkgs/website/src/components/SlowScroll.astro b/pkgs/website/src/components/SlowScroll.astro new file mode 100644 index 000000000..a65ee275b --- /dev/null +++ b/pkgs/website/src/components/SlowScroll.astro @@ -0,0 +1,58 @@ + diff --git a/pkgs/website/src/components/TestimonialCarousel.astro b/pkgs/website/src/components/TestimonialCarousel.astro index 4ced0744f..57f9b5fb6 100644 --- a/pkgs/website/src/components/TestimonialCarousel.astro +++ b/pkgs/website/src/components/TestimonialCarousel.astro @@ -94,14 +94,17 @@ const duplicatedTestimonials = [...testimonials, ...testimonials]; gap: 2rem; animation: scroll-left 120s linear infinite; width: fit-content; + /* Performance optimizations */ + will-change: transform; + transform: translateZ(0); /* Force GPU acceleration */ } @keyframes scroll-left { 0% { - transform: translateX(0); + transform: translate3d(0, 0, 0); } 100% { - transform: translateX(-50%); + transform: translate3d(-50%, 0, 0); } } @@ -109,14 +112,11 @@ const duplicatedTestimonials = [...testimonials, ...testimonials]; min-width: 400px; max-width: 400px; padding: 1.5rem; - background: linear-gradient( - 135deg, - color-mix(in srgb, var(--sl-color-accent-high) 4%, transparent) 0%, - color-mix(in srgb, var(--sl-color-accent-high) 1%, transparent) 100% - ); - border: 1px solid color-mix(in srgb, var(--sl-color-accent-high) 16%, transparent); + /* Simplified gradient for better performance */ + background: rgba(0, 123, 110, 0.04); + border: 1px solid rgba(0, 123, 110, 0.16); border-radius: 8px; - box-shadow: 0 4px 12px color-mix(in srgb, var(--sl-color-accent-high) 3%, transparent); + box-shadow: 0 4px 12px rgba(0, 123, 110, 0.03); flex-shrink: 0; display: flex; flex-direction: column; @@ -181,4 +181,12 @@ const duplicatedTestimonials = [...testimonials, ...testimonials]; font-size: 0.9rem; } } + + /* Disable animation for users who prefer reduced motion */ + @media (prefers-reduced-motion: reduce) { + .testimonial-track { + animation: none !important; + will-change: auto !important; + } + } diff --git a/pkgs/website/src/content/docs/index.mdx b/pkgs/website/src/content/docs/index.mdx index 29d915581..bd6c8d391 100644 --- a/pkgs/website/src/content/docs/index.mdx +++ b/pkgs/website/src/content/docs/index.mdx @@ -20,6 +20,7 @@ editUrl: false import { Card, CardGrid, LinkCard, Aside, Tabs, TabItem } from '@astrojs/starlight/components'; import TestimonialCarousel from '../../components/TestimonialCarousel.astro'; import CodeOverlay from '../../components/CodeOverlay.astro'; +import SlowScroll from '../../components/SlowScroll.astro';
@@ -422,7 +423,7 @@ Zero boilerplate required." .cta-card .sl-link-card { background: linear-gradient(135deg, color-mix(in srgb, var(--sl-color-accent-high) 6.4%, transparent) 0%, color-mix(in srgb, var(--sl-color-accent-high) 2.4%, transparent) 100%); - box-shadow: 0 0 40px color-mix(in srgb, var(--sl-color-accent-high) 5%, transparent), 0 4px 20px color-mix(in srgb, var(--sl-color-accent-high) 3%, transparent); + box-shadow: 0 0 20px color-mix(in srgb, var(--sl-color-accent-high) 5%, transparent), 0 4px 12px color-mix(in srgb, var(--sl-color-accent-high) 3%, transparent); border: 1px solid color-mix(in srgb, var(--sl-color-accent-high) 16%, transparent); transition: all 0.3s ease; position: relative; @@ -433,34 +434,11 @@ Zero boilerplate required." white-space: pre-line; } - .cta-card .sl-link-card::before { - content: ''; - position: absolute; - top: 1px; - left: -100%; - right: 1px; - bottom: 1px; - width: calc(100% - 2px); - height: calc(100% - 2px); - background: linear-gradient(90deg, - transparent 0%, - rgba(255, 255, 255, 0.004) 48%, - rgba(255, 255, 255, 0.008) 50%, - rgba(255, 255, 255, 0.004) 52%, - transparent 100% - ); - transition: left 0.25s ease; - z-index: 1; - } - + /* Removed continuous shine animation for better performance */ .cta-card .sl-link-card:hover { background: linear-gradient(135deg, color-mix(in srgb, var(--sl-color-accent-high) 7.5%, transparent) 0%, color-mix(in srgb, var(--sl-color-accent-high) 3%, transparent) 100%); - box-shadow: 0 0 60px color-mix(in srgb, var(--sl-color-accent-high) 8%, transparent), 0 4px 30px color-mix(in srgb, var(--sl-color-accent-high) 5%, transparent); - } - - .cta-card .sl-link-card:hover::before { - left: 100%; - transition: left 0.5s ease; + box-shadow: 0 0 30px color-mix(in srgb, var(--sl-color-accent-high) 8%, transparent), 0 4px 18px color-mix(in srgb, var(--sl-color-accent-high) 5%, transparent); + transform: translateY(-2px); } .quickstart-section { @@ -818,6 +796,7 @@ Zero boilerplate required." overflow: hidden !important; } + /* Mobile responsiveness for code blocks */ @media (max-width: 768px) { /* Smaller font for code blocks on mobile */ @@ -850,3 +829,6 @@ Zero boilerplate required." } } `} + + + diff --git a/pkgs/website/src/styles/global.css b/pkgs/website/src/styles/global.css index 5c22aeeed..41835ad5a 100644 --- a/pkgs/website/src/styles/global.css +++ b/pkgs/website/src/styles/global.css @@ -1,5 +1,6 @@ /* Prevent horizontal overflow */ -html, body { +html, +body { overflow-x: hidden; max-width: 100vw; } @@ -185,11 +186,11 @@ svg .secondary { .quickstart-section code { font-size: 1.1rem !important; } - + .testimonial-item blockquote { font-size: 1rem; } - + /* Hide carousel navigation arrows on mobile */ .testimonial-nav-arrows { display: none; @@ -209,15 +210,14 @@ svg .secondary { .quickstart-caption { font-size: 11px; } - + .testimonial-item blockquote { font-size: 0.9rem; } - + .testimonial-item cite { font-size: 0.8rem; } - } /* Responsive SVG images */ @@ -292,19 +292,42 @@ svg .secondary { } /* One-time attention nudge - more dramatic since it only happens once */ -.hire-sticker.nudge { - animation: attention-nudge 0.8s ease-in-out; -} +/* .hire-sticker.nudge { */ +/* animation: attention-nudge 0.8s ease-in-out; */ +/* } */ @keyframes attention-nudge { - 0% { transform: rotate(0deg) scale(1); } - 15% { transform: rotate(-6deg) scale(1.05); } - 30% { transform: rotate(6deg) scale(1.05); } - 45% { transform: rotate(-4deg) scale(1.02); } - 60% { transform: rotate(4deg) scale(1.02); } - 75% { transform: rotate(-2deg) scale(1.01); } - 90% { transform: rotate(1deg) scale(1); } - 100% { transform: rotate(0deg) scale(1); } + 0% { + transform: rotate(0deg) scale(1); + } + + 15% { + transform: rotate(-6deg) scale(1.05); + } + + 30% { + transform: rotate(6deg) scale(1.05); + } + + 45% { + transform: rotate(-4deg) scale(1.02); + } + + 60% { + transform: rotate(4deg) scale(1.02); + } + + 75% { + transform: rotate(-2deg) scale(1.01); + } + + 90% { + transform: rotate(1deg) scale(1); + } + + 100% { + transform: rotate(0deg) scale(1); + } } /* Hide on small screens */ @@ -325,60 +348,91 @@ svg .secondary { :root { /* Blue (Info/Note) - Steel blue (distinct yet harmonious) */ --sl-hue-blue: 220; - --sl-color-blue-low: hsl(220, 45%, 18%); /* Dark steel blue bg */ - --sl-color-blue: hsl(220, 65%, 55%); /* Steel blue */ - --sl-color-blue-high: hsl(220, 70%, 90%); /* Light steel blue */ + --sl-color-blue-low: hsl(220, 45%, 18%); + /* Dark steel blue bg */ + --sl-color-blue: hsl(220, 65%, 55%); + /* Steel blue */ + --sl-color-blue-high: hsl(220, 70%, 90%); + /* Light steel blue */ /* Purple (Tip) - Soft magenta-purple (stands out while harmonizing with teal) */ --sl-hue-purple: 280; - --sl-color-purple-low: hsl(280, 40%, 20%); /* Dark soft magenta-purple bg */ - --sl-color-purple: hsl(280, 55%, 62%); /* Soft magenta-purple */ - --sl-color-purple-high: hsl(280, 58%, 90%); /* Light soft magenta-purple */ + --sl-color-purple-low: hsl(280, 40%, 20%); + /* Dark soft magenta-purple bg */ + --sl-color-purple: hsl(280, 55%, 62%); + /* Soft magenta-purple */ + --sl-color-purple-high: hsl(280, 58%, 90%); + /* Light soft magenta-purple */ /* Orange (Caution) - Soft warm orange (warmer, harmonious) */ --sl-hue-orange: 35; - --sl-color-orange-low: hsl(35, 38%, 22%); /* Dark soft warm orange bg */ - --sl-color-orange: hsl(35, 60%, 68%); /* Soft warm orange */ - --sl-color-orange-high: hsl(35, 65%, 92%); /* Light soft warm orange */ + --sl-color-orange-low: hsl(35, 38%, 22%); + /* Dark soft warm orange bg */ + --sl-color-orange: hsl(35, 60%, 68%); + /* Soft warm orange */ + --sl-color-orange-high: hsl(35, 65%, 92%); + /* Light soft warm orange */ /* Red (Danger) - Bright red-orange (more intense) */ --sl-hue-red: 5; - --sl-color-red-low: hsl(5, 55%, 20%); /* Dark bright red-orange bg */ - --sl-color-red: hsl(5, 80%, 60%); /* Bright red-orange */ - --sl-color-red-high: hsl(5, 85%, 90%); /* Light bright red-orange */ + --sl-color-red-low: hsl(5, 55%, 20%); + /* Dark bright red-orange bg */ + --sl-color-red: hsl(5, 80%, 60%); + /* Bright red-orange */ + --sl-color-red-high: hsl(5, 85%, 90%); + /* Light bright red-orange */ /* Green (Success) - Aligned with primary theme (yellow-green) */ --sl-hue-green: 140; - --sl-color-green-low: hsl(140, 42%, 18%); /* Dark green bg */ - --sl-color-green: hsl(140, 60%, 50%); /* Green */ - --sl-color-green-high: hsl(140, 65%, 70%); /* Light green */ + --sl-color-green-low: hsl(140, 42%, 18%); + /* Dark green bg */ + --sl-color-green: hsl(140, 60%, 50%); + /* Green */ + --sl-color-green-high: hsl(140, 65%, 70%); + /* Light green */ } /* Light mode adjustments for harmonized colors */ :root[data-theme='light'] { /* Blue (Info/Note) */ - --sl-color-blue-high: hsl(220, 70%, 30%); /* Dark steel blue text */ - --sl-color-blue: hsl(220, 65%, 50%); /* Steel blue */ - --sl-color-blue-low: hsl(220, 60%, 92%); /* Light steel blue bg */ + --sl-color-blue-high: hsl(220, 70%, 30%); + /* Dark steel blue text */ + --sl-color-blue: hsl(220, 65%, 50%); + /* Steel blue */ + --sl-color-blue-low: hsl(220, 60%, 92%); + /* Light steel blue bg */ /* Purple (Tip) */ - --sl-color-purple-high: hsl(280, 58%, 32%); /* Dark soft magenta-purple text */ - --sl-color-purple: hsl(280, 55%, 58%); /* Soft magenta-purple */ - --sl-color-purple-low: hsl(280, 50%, 94%); /* Light soft magenta-purple bg */ + --sl-color-purple-high: hsl(280, + 58%, + 32%); + /* Dark soft magenta-purple text */ + --sl-color-purple: hsl(280, 55%, 58%); + /* Soft magenta-purple */ + --sl-color-purple-low: hsl(280, 50%, 94%); + /* Light soft magenta-purple bg */ /* Orange (Caution) */ - --sl-color-orange-high: hsl(35, 65%, 32%); /* Dark soft warm orange text */ - --sl-color-orange: hsl(35, 60%, 62%); /* Soft warm orange */ - --sl-color-orange-low: hsl(35, 58%, 94%); /* Light soft warm orange bg */ + --sl-color-orange-high: hsl(35, 65%, 32%); + /* Dark soft warm orange text */ + --sl-color-orange: hsl(35, 60%, 62%); + /* Soft warm orange */ + --sl-color-orange-low: hsl(35, 58%, 94%); + /* Light soft warm orange bg */ /* Red (Danger) */ - --sl-color-red-high: hsl(5, 85%, 35%); /* Dark bright red-orange text */ - --sl-color-red: hsl(5, 80%, 55%); /* Bright red-orange */ - --sl-color-red-low: hsl(5, 75%, 92%); /* Light bright red-orange bg */ + --sl-color-red-high: hsl(5, 85%, 35%); + /* Dark bright red-orange text */ + --sl-color-red: hsl(5, 80%, 55%); + /* Bright red-orange */ + --sl-color-red-low: hsl(5, 75%, 92%); + /* Light bright red-orange bg */ /* Green (Success) */ - --sl-color-green-high: hsl(140, 65%, 25%); /* Dark green text */ - --sl-color-green: hsl(140, 60%, 45%); /* Green */ - --sl-color-green-low: hsl(140, 55%, 92%); /* Light green bg */ + --sl-color-green-high: hsl(140, 65%, 25%); + /* Dark green text */ + --sl-color-green: hsl(140, 60%, 45%); + /* Green */ + --sl-color-green-low: hsl(140, 55%, 92%); + /* Light green bg */ } -