perf(pages): enable code splitting and lazy-load heavy dependencies (#487)
Some checks are pending
CI / test (push) Waiting to run
CI / cross-compile (amd64, darwin) (push) Waiting to run
CI / cross-compile (amd64, windows) (push) Waiting to run
CI / cross-compile (arm64, darwin) (push) Waiting to run
CI / cross-compile (arm64, linux) (push) Waiting to run
CI / cross-compile (arm64, windows) (push) Waiting to run
Deploy Pages / build (push) Waiting to run
Deploy Pages / deploy (push) Blocked by required conditions

Split route and vendor bundles to reduce critical-path JavaScript for the landing page. Defer Three.js until after first paint and load Mermaid only for documents that render diagrams.

Generate content-hashed assets for long-term caching and preserve the original shader appearance while displaying a lightweight fallback during deferred loading.

Fixes #457
This commit is contained in:
zhuangzhewei09 2026-07-27 17:33:05 +08:00 committed by GitHub
parent 112b17529b
commit bc32734cdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 188 additions and 93 deletions

View file

@ -1,11 +1,12 @@
import React, { useEffect } from 'react';
import React, { Suspense, useEffect } from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import LandingPage from './components/LandingPage';
import FeaturesPage from './pages/FeaturesPage';
import BenchmarkPage from './pages/BenchmarkPage';
import QuickStartPage from './pages/QuickStartPage';
import DocsPage from './pages/DocsPage';
import BlogPage from './pages/BlogPage';
const BenchmarkPage = React.lazy(() => import(/* webpackChunkName: "benchmark-page" */ './pages/BenchmarkPage'));
const QuickStartPage = React.lazy(() => import(/* webpackChunkName: "quickstart-page" */ './pages/QuickStartPage'));
const DocsPage = React.lazy(() => import(/* webpackChunkName: "docs-page" */ './pages/DocsPage'));
const BlogPage = React.lazy(() => import(/* webpackChunkName: "blog-page" */ './pages/BlogPage'));
const ScrollToTop: React.FC = () => {
const { pathname } = useLocation();
@ -19,15 +20,17 @@ const App: React.FC = () => {
return (
<>
<ScrollToTop />
<Routes>
<Route path="/" element={<LandingPage><FeaturesPage /></LandingPage>} />
<Route path="/benchmark" element={<LandingPage><BenchmarkPage /></LandingPage>} />
<Route path="/quickstart" element={<LandingPage><QuickStartPage /></LandingPage>} />
<Route path="/docs" element={<DocsPage />} />
<Route path="/docs/:slug" element={<DocsPage />} />
<Route path="/blog" element={<BlogPage />} />
<Route path="/blog/:slug" element={<BlogPage />} />
</Routes>
<Suspense fallback={<div style={{ minHeight: '100vh', background: '#000000' }} />}>
<Routes>
<Route path="/" element={<LandingPage><FeaturesPage /></LandingPage>} />
<Route path="/benchmark" element={<LandingPage><BenchmarkPage /></LandingPage>} />
<Route path="/quickstart" element={<LandingPage><QuickStartPage /></LandingPage>} />
<Route path="/docs" element={<DocsPage />} />
<Route path="/docs/:slug" element={<DocsPage />} />
<Route path="/blog" element={<BlogPage />} />
<Route path="/blog/:slug" element={<BlogPage />} />
</Routes>
</Suspense>
</>
);
};

View file

@ -1,12 +1,13 @@
import React, { useCallback, useState, useEffect } from 'react';
import React, { Suspense, useCallback, useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router-dom';
import { useTranslation } from '../i18n';
import { useResponsive } from '../hooks/useResponsive';
import ColorBends from './ColorBends';
import docDownloadIcon from '../assets/icons/doc-download-green.svg';
import copyIcon from '../assets/icons/icon-copy.svg';
const ColorBends = React.lazy(() => import(/* webpackChunkName: "color-bends" */ './ColorBends'));
const TC = {
brand: '#756BFF',
@ -123,6 +124,7 @@ const HeroSection: React.FC = () => {
const { isMobile, isTablet } = useResponsive();
const [toastVisible, setToastVisible] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [showShaderBackground, setShowShaderBackground] = useState(false);
const showToast = (message: string) => {
setToastMessage(message);
@ -164,6 +166,30 @@ const HeroSection: React.FC = () => {
return () => clearTimeout(timer);
}, [toastVisible]);
useEffect(() => {
// Wait until after the first paint before loading the heavy shader chunk.
let secondFrame: number | undefined;
const firstFrame = requestAnimationFrame(() => {
secondFrame = requestAnimationFrame(() => setShowShaderBackground(true));
});
return () => {
cancelAnimationFrame(firstFrame);
if (secondFrame !== undefined) cancelAnimationFrame(secondFrame);
};
}, []);
const shaderFallback = (
<div
style={{
position: 'absolute',
inset: 0,
zIndex: 0,
background: 'radial-gradient(circle at 50% 20%, #0d750d 0%, #042e04 38%, #000000 78%)',
}}
/>
);
return (
<>
<section
@ -179,29 +205,34 @@ const HeroSection: React.FC = () => {
}}
>
{/* Shader Background */}
<ColorBends
style={{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
zIndex: 0,
}}
colors={['#0d750d', '#042e04', '#066020']}
rotation={90}
speed={0.23}
scale={1.2}
frequency={1}
warpStrength={1}
mouseInfluence={1}
noise={0.33}
parallax={0.45}
iterations={1}
intensity={0.8}
bandWidth={6}
transparent
/>
{!showShaderBackground && shaderFallback}
{showShaderBackground && (
<Suspense fallback={shaderFallback}>
<ColorBends
style={{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
zIndex: 0,
}}
colors={['#0d750d', '#042e04', '#066020']}
rotation={90}
speed={0.23}
scale={1.2}
frequency={1}
warpStrength={1}
mouseInfluence={1}
noise={0.33}
parallax={0.45}
iterations={1}
intensity={0.8}
bandWidth={6}
transparent
/>
</Suspense>
)}
{/* Gradient overlay */}
<div

View file

@ -2,39 +2,55 @@ import React, { useMemo, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import { Marked, Renderer } from 'marked';
import DOMPurify from 'dompurify';
import mermaid from 'mermaid';
import { useTranslation } from '../i18n';
import { useCopyToast } from '../hooks/useCopyToast';
import copyIcon from '../assets/icons/icon-copy.svg';
import { generateHeadingId } from '../utils/headingId';
// Initialize mermaid with dark theme
mermaid.initialize({
startOnLoad: false,
// 'strict' makes mermaid sanitize its own SVG output (DOMPurify internally):
// safe label HTML like <b>/<span> is kept, scripts/handlers are stripped.
// This is why we can inject the returned SVG directly below without re-sanitizing.
securityLevel: 'strict',
theme: 'dark',
themeVariables: {
primaryColor: '#1a1a2e',
primaryTextColor: 'rgba(255,255,255,0.85)',
primaryBorderColor: 'rgba(255,255,255,0.2)',
lineColor: 'rgba(255,255,255,0.4)',
secondaryColor: '#16213e',
tertiaryColor: '#0f3460',
background: '#000000',
mainBkg: 'rgba(255,255,255,0.04)',
nodeBorder: 'rgba(255,255,255,0.16)',
clusterBkg: 'rgba(255,255,255,0.02)',
titleColor: '#FFFFFF',
edgeLabelBackground: '#000000',
},
flowchart: {
htmlLabels: true,
curve: 'basis',
},
});
type Mermaid = typeof import('mermaid')['default'];
let mermaidPromise: Promise<Mermaid> | null = null;
function loadMermaid(): Promise<Mermaid> {
if (!mermaidPromise) {
mermaidPromise = import('mermaid')
.then(({ default: mermaid }) => {
mermaid.initialize({
startOnLoad: false,
// 'strict' makes mermaid sanitize its own SVG output (DOMPurify internally):
// safe label HTML like <b>/<span> is kept, scripts/handlers are stripped.
// This is why we can inject the returned SVG directly below without re-sanitizing.
securityLevel: 'strict',
theme: 'dark',
themeVariables: {
primaryColor: '#1a1a2e',
primaryTextColor: 'rgba(255,255,255,0.85)',
primaryBorderColor: 'rgba(255,255,255,0.2)',
lineColor: 'rgba(255,255,255,0.4)',
secondaryColor: '#16213e',
tertiaryColor: '#0f3460',
background: '#000000',
mainBkg: 'rgba(255,255,255,0.04)',
nodeBorder: 'rgba(255,255,255,0.16)',
clusterBkg: 'rgba(255,255,255,0.02)',
titleColor: '#FFFFFF',
edgeLabelBackground: '#000000',
},
flowchart: {
htmlLabels: true,
curve: 'basis',
},
});
return mermaid;
})
.catch((error) => {
// Allow a later navigation to retry after a transient chunk-load failure.
mermaidPromise = null;
throw error;
});
}
return mermaidPromise;
}
interface MarkdownRendererProps {
content: string;
@ -105,33 +121,47 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content }) => {
const mermaidBlocks = containerRef.current.querySelectorAll('code.language-mermaid');
if (mermaidBlocks.length === 0) return;
const renderPromises = Array.from(mermaidBlocks).map(async (block) => {
const pre = block.parentElement;
if (!pre) return;
const code = block.textContent || '';
const renderMermaidBlocks = async () => {
try {
const id = `mermaid-diagram-${crypto.randomUUID()}`;
const { svg } = await mermaid.render(id, code);
const mermaid = await loadMermaid();
if (cancelled) return;
// Replace the <pre> with rendered SVG. The SVG is produced by mermaid with
// securityLevel:'strict' (see initialize above), which already sanitizes its
// output. Re-running DOMPurify over the whole SVG breaks it (namespaces,
// inline <style>, foreignObject labels), so we inject mermaid's trusted
// output directly.
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-rendered';
// codeql[js/xss-through-dom] -- svg is derived from user-controlled mermaid code, but mermaid
// renders it with securityLevel:'strict' (see initialize above), which sanitizes the output via
// DOMPurify (scripts/handlers stripped). The trust boundary relies on that setting staying 'strict'.
wrapper.innerHTML = svg;
pre.replaceWith(wrapper);
for (const block of Array.from(mermaidBlocks)) {
const pre = block.parentElement;
if (!pre) continue;
const code = block.textContent || '';
try {
const id = `mermaid-diagram-${crypto.randomUUID()}`;
const { svg } = await mermaid.render(id, code);
if (cancelled) return;
// Replace the <pre> with rendered SVG. The SVG is produced by mermaid with
// securityLevel:'strict' (configured in loadMermaid), which already sanitizes
// its output. Re-running DOMPurify over the whole SVG breaks it (namespaces,
// inline <style>, foreignObject labels), so we inject mermaid's trusted output.
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-rendered';
// codeql[js/xss-through-dom] -- svg is derived from user-controlled mermaid code, but mermaid
// renders it with securityLevel:'strict' (see loadMermaid), which sanitizes the output via
// DOMPurify (scripts/handlers stripped). The trust boundary relies on that setting staying 'strict'.
wrapper.innerHTML = svg;
pre.replaceWith(wrapper);
} catch (e) {
if (cancelled) return;
// If rendering fails, show the code block normally
(block as HTMLElement).style.display = 'block';
console.warn('[Mermaid] render failed:', e);
}
}
} catch (e) {
if (cancelled) return;
// If rendering fails, show the code block normally
(block as HTMLElement).style.display = 'block';
console.warn('[Mermaid] render failed:', e);
for (const block of Array.from(mermaidBlocks)) {
(block as HTMLElement).style.display = 'block';
}
console.warn('[Mermaid] failed to load:', e);
}
});
};
void renderMermaidBlocks();
return () => { cancelled = true; };
}, [html]);

View file

@ -1,14 +1,45 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
mode: isProduction ? 'production' : 'development',
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/'
filename: '[name].[contenthash:8].bundle.js',
chunkFilename: '[name].[contenthash:8].chunk.js',
publicPath: '/',
clean: true
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom|scheduler)[\\/]/,
name: 'react',
priority: 30,
enforce: true
},
three: {
test: /[\\/]node_modules[\\/]three[\\/]/,
name: 'three',
chunks: 'async',
priority: 20,
enforce: true
},
mermaid: {
test: /[\\/]node_modules[\\/]mermaid[\\/]/,
name: 'mermaid',
chunks: 'async',
priority: 20,
enforce: true
}
}
}
},
module: {
rules: [
@ -19,7 +50,7 @@ module.exports = {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-react', { development: process.env.NODE_ENV !== 'production' }],
['@babel/preset-react', { development: !isProduction }],
'@babel/preset-env',
'@babel/preset-typescript'
]