-
Notifications
You must be signed in to change notification settings - Fork 867
perf(pages): enable code splitting and lazy-load heavy dependencies #487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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,29 @@ const HeroSection: React.FC = () => { | |
| return () => clearTimeout(timer); | ||
| }, [toastVisible]); | ||
|
|
||
| useEffect(() => { | ||
| let secondFrame: number | undefined; | ||
| const firstFrame = requestAnimationFrame(() => { | ||
| secondFrame = requestAnimationFrame(() => setShowShaderBackground(true)); | ||
| }); | ||
|
Comment on lines
+171
to
+173
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The double |
||
|
|
||
| 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 +204,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 | ||
| /> | ||
| {shaderFallback} | ||
| {showShaderBackground && ( | ||
| <Suspense fallback={null}> | ||
| <ColorBends | ||
|
Comment on lines
+208
to
+210
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The lazy-loaded |
||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,38 +2,54 @@ import React, { useMemo, useEffect, useRef, useState, useCallback, useId } from | |
| import ReactDOM from 'react-dom'; | ||
| import { Marked, Renderer } from 'marked'; | ||
| import DOMPurify from 'dompurify'; | ||
| import mermaid from 'mermaid'; | ||
| import { useTranslation } from '../i18n'; | ||
| 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; | ||
|
|
@@ -132,33 +148,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); | ||
|
Comment on lines
+156
to
+162
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rendering of multiple mermaid diagrams was changed from parallel ( Consider using a concurrency-limited approach (e.g., render 2–3 at a time) or document why fully sequential rendering is necessary. If mermaid's |
||
| 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]); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing Error Boundary for lazy-loaded components
React.lazy()will throw an error if the chunk fails to load (e.g., due to network issues, server downtime, or a new deployment invalidating old chunk URLs). Without an error boundary wrapping theSuspense, this will result in an unhandled crash and a blank page with no recovery path for the user.Consider adding an error boundary component around the
Suspense(or at a higher level) to gracefully handle chunk loading failures, e.g.: