1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-05 17:53:12 +02:00

fix: largest contentful paint

This commit is contained in:
FredrikOseberg 2025-08-11 19:00:25 +02:00
parent 04453c5663
commit 59a135f704
No known key found for this signature in database
GPG Key ID: 282FD8A6D8F9BCF0
5 changed files with 738 additions and 1 deletions

View File

@ -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;
}
}

View File

@ -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 (
<Head>
{/* Preload font files for faster loading */}
<link
rel="preload"
href="/fonts/Sen-Regular.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Sen-Bold.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{/* Font-face declarations with font-display: swap */}
<style>{`
@font-face {
font-family: 'Sen';
src: url('/fonts/Sen-Regular.woff2') format('woff2'),
url('/fonts/Sen-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap; /* Critical: shows text immediately */
}
@font-face {
font-family: 'Sen';
src: url('/fonts/Sen-Bold.woff2') format('woff2'),
url('/fonts/Sen-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Sen';
src: url('/fonts/Sen-SemiBold.woff2') format('woff2'),
url('/fonts/Sen-SemiBold.woff') format('woff');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* Fallback for older browsers */
@supports not (font-display: swap) {
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, sans-serif;
}
}
`}</style>
</Head>
);
}

View File

@ -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 = `
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text" style="width: 80%"></div>
`;
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 (
<Head>
<style>{`
/* Layout Stability Styles */
/* Reserve space for navbar */
.navbar.layout-reserved {
min-height: 56px;
contain: layout style;
}
/* Reserve space for sidebar */
aside.layout-reserved {
min-width: 250px;
contain: layout style;
}
/* Stabilize main container */
main.layout-stabilized {
min-height: calc(100vh - 56px);
contain: layout;
will-change: auto;
}
/* Prevent layout shifts from async content */
article {
min-height: 400px;
contain: layout style;
}
/* Image stability */
img {
max-width: 100%;
height: auto;
display: block;
background: #f0f0f0;
}
img[data-stabilized] {
background: transparent;
}
/* Skeleton loading styles */
.skeleton-loader {
padding: 1rem;
animation: fade-in 0.3s ease-in;
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 4px;
margin-bottom: 1rem;
}
.skeleton-title {
height: 2rem;
width: 60%;
margin-bottom: 1.5rem;
}
.skeleton-text {
height: 1rem;
width: 100%;
margin-bottom: 0.5rem;
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* Prevent CLS from font loading */
body {
font-synthesis: none;
text-rendering: optimizeLegibility;
}
/* Stabilize buttons and interactive elements */
button, a.button {
min-height: 36px;
contain: layout style;
}
/* Stabilize form elements */
input, textarea, select {
min-height: 36px;
contain: layout style;
}
/* Code blocks stability */
pre {
min-height: 60px;
contain: layout;
overflow: auto;
}
/* Table stability */
table {
table-layout: fixed;
width: 100%;
contain: layout;
}
/* Mobile stability */
@media (max-width: 996px) {
main.layout-stabilized {
min-height: calc(100vh - 60px);
}
.navbar.layout-reserved {
min-height: 60px;
}
}
/* Prevent layout shift from lazy-loaded content */
[data-lazy-load] {
min-height: 100px;
contain: layout;
}
/* Optimize Cumulative Layout Shift */
* {
/* Prevent margin collapse issues */
margin-top: 0;
}
h1, h2, h3, h4, h5, h6, p, ul, ol {
margin-bottom: 1rem;
}
`}</style>
</Head>
);
}

View File

@ -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 (
<style
dangerouslySetInnerHTML={{
__html: `
/* Inline critical styles for immediate paint */
/* This prevents render-blocking */
@import url('/src/css/critical.css');
`
}}
data-critical="true"
/>
);
}

View File

@ -1,5 +1,11 @@
import React, { useEffect } from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import OptimizedStyles from './OptimizedStyles';
import FontLoader from './FontLoader';
import LayoutStabilizer from './LayoutStabilizer';
// Import critical CSS directly for immediate availability
import criticalCSS from '!raw-loader!../css/critical.css';
export default function Root({ children }: { children: React.ReactNode }) {
useEffect(() => {
@ -83,5 +89,21 @@ export default function Root({ children }: { children: React.ReactNode }) {
};
}, []);
return <>{children}</>;
return (
<>
{/* Inline critical CSS for instant rendering */}
<style
dangerouslySetInnerHTML={{ __html: criticalCSS }}
data-critical='true'
/>
{/* Performance optimization components */}
<OptimizedStyles />
<FontLoader />
<LayoutStabilizer />
{/* Main app content */}
{children}
</>
);
}