mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-05 17:53:12 +02:00
fix: fonts
This commit is contained in:
parent
ff2f9ed97e
commit
5ab1aec408
@ -2,11 +2,9 @@
|
||||
/* Only includes above-the-fold styles needed for initial paint */
|
||||
/* This CSS should not conflict with main styles - only prevent layout shift */
|
||||
|
||||
/* Import Google Fonts with optimal loading */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Sen:wght@400;600;700&display=swap');
|
||||
|
||||
/* Prevent layout shift - reserve space */
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
@ -30,7 +28,7 @@ html, body {
|
||||
.navbar .navbar__brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Reserve space for GitHub star button to prevent shift */
|
||||
.header-github-link.navbar-link-outlined {
|
||||
min-width: 120px; /* Reserve space for "Star 12.5k" */
|
||||
@ -38,7 +36,7 @@ html, body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure navbar items don't shift */
|
||||
.navbar__items--right {
|
||||
min-width: 450px; /* Stabilize right navbar width */
|
||||
@ -85,9 +83,9 @@ main {
|
||||
aside[class*="docSidebarContainer"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,168 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -1,253 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
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 and any styles that are already optimized
|
||||
if (link.href.includes('critical') ||
|
||||
link.getAttribute('data-critical') === 'true' ||
|
||||
link.getAttribute('data-optimized') === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as optimized to avoid re-processing
|
||||
link.setAttribute('data-optimized', 'true');
|
||||
|
||||
// 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.getAttribute('data-critical') &&
|
||||
!link.href.includes('critical')) {
|
||||
link.setAttribute('data-optimized', 'true');
|
||||
// Make it non-blocking
|
||||
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();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// This component handles non-critical CSS optimization only
|
||||
// Critical CSS is handled in Root.tsx
|
||||
return null;
|
||||
}
|
Loading…
Reference in New Issue
Block a user