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:
parent
04453c5663
commit
59a135f704
134
website/src/css/critical.css
Normal file
134
website/src/css/critical.css
Normal 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;
|
||||
}
|
||||
}
|
168
website/src/theme/FontLoader.tsx
Normal file
168
website/src/theme/FontLoader.tsx
Normal 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>
|
||||
);
|
||||
}
|
253
website/src/theme/LayoutStabilizer.tsx
Normal file
253
website/src/theme/LayoutStabilizer.tsx
Normal 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>
|
||||
);
|
||||
}
|
160
website/src/theme/OptimizedStyles.tsx
Normal file
160
website/src/theme/OptimizedStyles.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user