diff --git a/website/src/css/critical.css b/website/src/css/critical.css new file mode 100644 index 0000000000..f78a60bb05 --- /dev/null +++ b/website/src/css/critical.css @@ -0,0 +1,134 @@ +/* Critical CSS - Inline for immediate rendering */ +/* Only includes above-the-fold styles needed for initial paint */ + +:root { + --unleash-color-green: #1a4049; + --unleash-color-purple: #635dc5; + --ifm-font-family-base: "Sen", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif; + --ifm-font-size-base: 15px; + --ifm-background-color: #fff; + --ifm-font-color-base: #202021; +} + +/* Prevent layout shift - reserve space */ +html, body { + margin: 0; + padding: 0; + overflow-x: hidden; +} + +/* Navbar space reservation */ +.navbar { + height: 56px; + position: sticky; + top: 0; + z-index: 100; + background: var(--ifm-background-color); +} + +/* Main container - prevent CLS */ +.docMainContainer_TBSr, +main[class*="docMainContainer"] { + min-height: 100vh; + contain: layout style; + display: block; + padding: 0 1rem; + max-width: 100%; +} + +/* Content wrapper */ +.container { + max-width: 1140px; + margin: 0 auto; + padding: 0 1rem; +} + +/* Basic typography for immediate readability */ +h1, h2, h3, h4, h5, h6 { + font-family: var(--ifm-font-family-base); + font-weight: 700; + line-height: 1.2; + color: var(--ifm-font-color-base); + margin-top: 0; + margin-bottom: 0.5rem; +} + +h1 { font-size: 2.5rem; } +h2 { font-size: 2rem; } +h3 { font-size: 1.5rem; } + +p { + font-family: var(--ifm-font-family-base); + font-size: var(--ifm-font-size-base); + line-height: 1.6; + color: var(--ifm-font-color-base); + margin: 0 0 1rem; +} + +/* Links */ +a { + color: var(--unleash-color-purple); + text-decoration: none; +} + +/* Code blocks */ +code { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 0.875em; + background: rgba(99, 93, 197, 0.1); + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; +} + +/* Sidebar reservation */ +.docSidebarContainer_YfHR, +aside[class*="docSidebarContainer"] { + width: 250px; + position: sticky; + top: 56px; + height: calc(100vh - 56px); + overflow-y: auto; +} + +/* Article content */ +article { + min-height: 50vh; + padding: 2rem 0; +} + +/* Prevent FOUC for theme */ +[data-theme="dark"] { + --ifm-background-color: #222130; + --ifm-font-color-base: #f5f5f5; +} + +/* Loading skeleton animation */ +@keyframes skeleton-loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.skeleton { + background: linear-gradient( + 90deg, + rgba(190, 190, 190, 0.2) 25%, + rgba(190, 190, 190, 0.3) 50%, + rgba(190, 190, 190, 0.2) 75% + ); + background-size: 200% 100%; + animation: skeleton-loading 1.5s infinite; +} + +/* Mobile responsiveness */ +@media (max-width: 996px) { + .docSidebarContainer_YfHR, + aside[class*="docSidebarContainer"] { + display: none; + } + + .docMainContainer_TBSr, + main[class*="docMainContainer"] { + width: 100%; + padding: 0 0.5rem; + } +} \ No newline at end of file diff --git a/website/src/theme/FontLoader.tsx b/website/src/theme/FontLoader.tsx new file mode 100644 index 0000000000..e775dd2f1a --- /dev/null +++ b/website/src/theme/FontLoader.tsx @@ -0,0 +1,168 @@ +import React, { useEffect } from 'react'; +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import Head from '@docusaurus/Head'; + +export default function FontLoader(): React.JSX.Element { + useEffect(() => { + if (!ExecutionEnvironment.canUseDOM) { + return; + } + + // Font loading strategy + const loadFonts = async () => { + // Check if fonts are already loaded + if (document.fonts && document.fonts.ready) { + try { + await document.fonts.ready; + document.documentElement.classList.add('fonts-loaded'); + } catch (error) { + console.error('Font loading error:', error); + } + } + + // Create font face with font-display: swap + const senFontFace = new FontFace( + 'Sen', + `url('/fonts/Sen-Regular.woff2') format('woff2'), + url('/fonts/Sen-Regular.woff') format('woff')`, + { + weight: '400', + style: 'normal', + display: 'swap' // Critical for preventing invisible text + } + ); + + const senBoldFontFace = new FontFace( + 'Sen', + `url('/fonts/Sen-Bold.woff2') format('woff2'), + url('/fonts/Sen-Bold.woff') format('woff')`, + { + weight: '700', + style: 'normal', + display: 'swap' + } + ); + + // Load fonts asynchronously + try { + const loadedFonts = await Promise.all([ + senFontFace.load(), + senBoldFontFace.load() + ]); + + // Add fonts to document + loadedFonts.forEach(font => { + (document.fonts as any).add(font); + }); + + // Mark fonts as loaded + document.documentElement.classList.add('custom-fonts-loaded'); + } catch (error) { + console.error('Failed to load custom fonts:', error); + // Fallback to system fonts is automatic due to font stack + } + }; + + // Load fonts with requestIdleCallback for better performance + if ('requestIdleCallback' in window) { + window.requestIdleCallback(() => loadFonts(), { timeout: 3000 }); + } else { + // Fallback for browsers without requestIdleCallback + setTimeout(loadFonts, 100); + } + + // Add CSS for font loading states + const fontLoadingStyles = document.createElement('style'); + fontLoadingStyles.textContent = ` + /* Use system fonts initially for faster paint */ + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + } + + /* Apply custom fonts when loaded */ + .custom-fonts-loaded body { + font-family: "Sen", -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + } + + /* Smooth transition when fonts load */ + body, h1, h2, h3, h4, h5, h6, p, a, li { + transition: font-family 0.2s ease-in-out; + } + + /* Prevent layout shift from font loading */ + h1, h2, h3, h4, h5, h6 { + font-synthesis: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + `; + document.head.appendChild(fontLoadingStyles); + + return () => { + // Cleanup if needed + if (fontLoadingStyles.parentNode) { + fontLoadingStyles.parentNode.removeChild(fontLoadingStyles); + } + }; + }, []); + + return ( + + {/* Preload font files for faster loading */} + + + + {/* Font-face declarations with font-display: swap */} + + + ); +} \ No newline at end of file diff --git a/website/src/theme/LayoutStabilizer.tsx b/website/src/theme/LayoutStabilizer.tsx new file mode 100644 index 0000000000..b5d3eae018 --- /dev/null +++ b/website/src/theme/LayoutStabilizer.tsx @@ -0,0 +1,253 @@ +import React, { useEffect } from 'react'; +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import Head from '@docusaurus/Head'; + +export default function LayoutStabilizer(): React.JSX.Element { + useEffect(() => { + if (!ExecutionEnvironment.canUseDOM) { + return; + } + + // Add layout stability classes + const stabilizeLayout = () => { + // Reserve space for navbar + const navbar = document.querySelector('.navbar'); + if (navbar && !navbar.classList.contains('layout-reserved')) { + navbar.classList.add('layout-reserved'); + } + + // Reserve space for sidebar + const sidebar = document.querySelector('aside[class*="docSidebarContainer"]'); + if (sidebar && !sidebar.classList.contains('layout-reserved')) { + sidebar.classList.add('layout-reserved'); + } + + // Stabilize main container + const mainContainer = document.querySelector('main[class*="docMainContainer"]'); + if (mainContainer && !mainContainer.classList.contains('layout-stabilized')) { + mainContainer.classList.add('layout-stabilized'); + } + + // Add skeleton loading for slow content + const addSkeletonLoading = () => { + const contentContainers = document.querySelectorAll('article:empty, .markdown:empty'); + contentContainers.forEach(container => { + if (!container.querySelector('.skeleton-loader')) { + const skeleton = document.createElement('div'); + skeleton.className = 'skeleton-loader'; + skeleton.innerHTML = ` +
+
+
+
+ `; + container.appendChild(skeleton); + } + }); + }; + + addSkeletonLoading(); + + // Observe images for lazy loading with proper dimensions + const images = document.querySelectorAll('img:not([data-stabilized])'); + images.forEach(img => { + const imgElement = img as HTMLImageElement; + + // If image has intrinsic dimensions, reserve space + if (imgElement.naturalWidth && imgElement.naturalHeight) { + const aspectRatio = imgElement.naturalHeight / imgElement.naturalWidth; + imgElement.style.aspectRatio = `${imgElement.naturalWidth} / ${imgElement.naturalHeight}`; + } else { + // Set a default aspect ratio for common image types + if (imgElement.src.includes('screenshot') || imgElement.src.includes('demo')) { + imgElement.style.aspectRatio = '16 / 9'; + } else if (imgElement.src.includes('logo') || imgElement.src.includes('icon')) { + imgElement.style.aspectRatio = '1 / 1'; + } else { + imgElement.style.aspectRatio = '4 / 3'; // Default aspect ratio + } + } + + imgElement.setAttribute('data-stabilized', 'true'); + imgElement.loading = 'lazy'; // Enable native lazy loading + imgElement.decoding = 'async'; // Async decoding for better performance + }); + }; + + // Run stabilization immediately + stabilizeLayout(); + + // Re-run on route changes (for SPA navigation) + const observer = new MutationObserver(() => { + stabilizeLayout(); + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + // Monitor viewport changes + let resizeTimer: NodeJS.Timeout; + const handleResize = () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + stabilizeLayout(); + }, 250); + }; + + window.addEventListener('resize', handleResize, { passive: true }); + + // Cleanup + return () => { + observer.disconnect(); + window.removeEventListener('resize', handleResize); + clearTimeout(resizeTimer); + }; + }, []); + + return ( + + + + ); +} \ No newline at end of file diff --git a/website/src/theme/OptimizedStyles.tsx b/website/src/theme/OptimizedStyles.tsx new file mode 100644 index 0000000000..4b75c29103 --- /dev/null +++ b/website/src/theme/OptimizedStyles.tsx @@ -0,0 +1,160 @@ +import React, { useEffect } from 'react'; +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + +export default function OptimizedStyles() { + useEffect(() => { + if (!ExecutionEnvironment.canUseDOM) { + return; + } + + // Function to load CSS asynchronously + const loadStylesheet = (href: string, media: string = 'all') => { + // Check if already loaded + if (document.querySelector(`link[href="${href}"]`)) { + return; + } + + // Create preload link + const preloadLink = document.createElement('link'); + preloadLink.rel = 'preload'; + preloadLink.as = 'style'; + preloadLink.href = href; + + // Create actual stylesheet link + const styleLink = document.createElement('link'); + styleLink.rel = 'stylesheet'; + styleLink.href = href; + styleLink.media = media; + + // Once preloaded, apply the stylesheet + preloadLink.onload = () => { + preloadLink.onload = null; + // Switch from preload to stylesheet + preloadLink.rel = 'stylesheet'; + }; + + // Fallback for browsers that don't support preload + const noscriptFallback = document.createElement('noscript'); + const fallbackLink = document.createElement('link'); + fallbackLink.rel = 'stylesheet'; + fallbackLink.href = href; + noscriptFallback.appendChild(fallbackLink); + + // Add to document head + document.head.appendChild(preloadLink); + document.head.appendChild(noscriptFallback); + + // Also add regular link with print media to load in background + const printLink = document.createElement('link'); + printLink.rel = 'stylesheet'; + printLink.href = href; + printLink.media = 'print'; + printLink.onload = function() { + (this as HTMLLinkElement).media = media; + }; + document.head.appendChild(printLink); + }; + + // Load non-critical styles after initial render + const loadNonCriticalStyles = () => { + // Load additional styles based on viewport + if (window.innerWidth > 996) { + // Desktop-specific styles + loadStylesheet('/assets/css/desktop-enhancements.css', 'screen and (min-width: 997px)'); + } else { + // Mobile-specific styles + loadStylesheet('/assets/css/mobile-enhancements.css', 'screen and (max-width: 996px)'); + } + + // Load animation and interaction styles + loadStylesheet('/assets/css/animations.css', 'all'); + + // Load theme-specific styles + const theme = document.documentElement.getAttribute('data-theme'); + if (theme === 'dark') { + loadStylesheet('/assets/css/dark-theme.css', 'all'); + } + }; + + // Use requestIdleCallback if available, otherwise setTimeout + if ('requestIdleCallback' in window) { + window.requestIdleCallback(loadNonCriticalStyles, { timeout: 2000 }); + } else { + setTimeout(loadNonCriticalStyles, 100); + } + + // Optimize existing stylesheets + const optimizeExistingStyles = () => { + const allStylesheets = document.querySelectorAll('link[rel="stylesheet"]'); + allStylesheets.forEach((stylesheet) => { + const link = stylesheet as HTMLLinkElement; + // Skip critical styles + if (link.href.includes('critical') || link.getAttribute('data-critical') === 'true') { + return; + } + + // Convert blocking stylesheets to non-blocking + if (!link.media || link.media === 'all') { + // Temporarily set to print to make non-blocking + const originalMedia = link.media || 'all'; + link.media = 'print'; + link.onload = function() { + (this as HTMLLinkElement).media = originalMedia; + }; + } + }); + }; + + // Run optimization immediately + optimizeExistingStyles(); + + // Monitor for dynamically added stylesheets + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + mutation.addedNodes.forEach((node) => { + if (node.nodeName === 'LINK') { + const link = node as HTMLLinkElement; + if (link.rel === 'stylesheet' && !link.getAttribute('data-optimized')) { + link.setAttribute('data-optimized', 'true'); + // Make it non-blocking if it's not critical + if (!link.href.includes('critical')) { + const originalMedia = link.media || 'all'; + link.media = 'print'; + link.onload = function() { + (this as HTMLLinkElement).media = originalMedia; + }; + } + } + } + }); + } + }); + }); + + // Start observing the document head for changes + observer.observe(document.head, { + childList: true, + subtree: false + }); + + // Cleanup + return () => { + observer.disconnect(); + }; + }, []); + + // Inline critical CSS for immediate rendering + return ( +