mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-08-11 09:44:46 +00:00
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
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:
parent
112b17529b
commit
bc32734cdf
4 changed files with 188 additions and 93 deletions
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue