Skip to content

perf(pages): enable code splitting and lazy-load heavy dependencies#487

Open
chacha923 wants to merge 1 commit into
alibaba:mainfrom
chacha923:perf/pages-code-splitting-457
Open

perf(pages): enable code splitting and lazy-load heavy dependencies#487
chacha923 wants to merge 1 commit into
alibaba:mainfrom
chacha923:perf/pages-code-splitting-457

Conversation

@chacha923

@chacha923 chacha923 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

Reduce the landing page's initial JavaScript payload by splitting route and vendor chunks and loading Three.js and Mermaid only when needed.

Changes

  • Add content-hashed entry and chunk filenames, splitChunks, and a separate runtime chunk
  • Lazy-load non-landing routes and defer the Three.js hero background until after first paint
  • Dynamically import Mermaid only for documents containing Mermaid code blocks
  • Generate gzip and Brotli assets during production builds

Bundle Impact

Metric Before After
Initial JavaScript (uncompressed) 2,106,383 B 302,659 B
Lighthouse transfer size 629 KiB 321 KiB

Lighthouse

Desktop preset with default throttling in Lighthouse 13.4.1, measured on the same machine, Chrome version, and static server.

open-codereview.ai could not be loaded from the test environment because the TLS connection was closed. The before measurement therefore uses an unmodified origin/main production build served locally, while the after measurement uses this branch's production build under identical conditions.

Before (origin/main) After (this PR)
Metric Before After
Performance 99 100
First Contentful Paint 0.5 s 0.5 s
Largest Contentful Paint 0.9 s 0.6 s
Total Blocking Time 10 ms 0 ms
Speed Index 0.8 s 0.8 s
Cumulative Layout Shift 0 0

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring (no functional changes)
  • Documentation update
  • CI / Build / Tooling

How Has This Been Tested?

  • npm run typecheck in pages/ with Node.js 24
  • npm run build in pages/ with Node.js 24
  • Clean install, typecheck, and production build with Node.js 24 and npm 11
  • Manually verified the landing page and all SPA routes, including Mermaid diagrams and the 404.html fallback
  • Verified that Three.js and Mermaid are excluded from the initial HTML and loaded on demand
  • Lighthouse Desktop comparison with default throttling

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or my feature works (not applicable; build output and browser behavior were verified directly)
  • New and existing checks pass locally with my changes
  • Documentation is not required for this build-only optimization
  • I have signed the CLA

Related Issues

Closes #457

lighthouse-production-before-457 lighthouse-after-457

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 5 issue(s) in this PR.

  • ✅ Successfully posted inline: 5 comment(s)

Comment thread pages/src/App.tsx
Comment on lines +23 to +24
<Suspense fallback={<div style={{ minHeight: '100vh', background: '#000000' }} />}>
<Routes>

Copy link
Copy Markdown
Contributor

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 the Suspense, 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.:

<ErrorBoundary fallback={<div>Failed to load page. Please refresh.</div>}>
  <Suspense fallback={...}>
    <Routes>...</Routes>
  </Suspense>
</ErrorBoundary>

Comment on lines +208 to +210
{showShaderBackground && (
<Suspense fallback={null}>
<ColorBends

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lazy-loaded ColorBends component has no error boundary wrapping it. If the dynamic import fails (e.g., network error, missing chunk after deployment), React will throw an unhandled error that can crash the entire HeroSection or page. Since there is no ErrorBoundary component anywhere in the project, consider adding one around the <Suspense> block so that on chunk load failure the component gracefully degrades to the static gradient fallback (shaderFallback) which is already rendered underneath.

Comment on lines +171 to +173
const firstFrame = requestAnimationFrame(() => {
secondFrame = requestAnimationFrame(() => setShowShaderBackground(true));
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The double requestAnimationFrame pattern is a non-obvious performance optimization technique used to defer rendering past the first paint. Future maintainers may not understand why two frames are needed and might accidentally simplify or remove this logic. Consider adding a brief comment explaining the intent, e.g., // Wait two animation frames to ensure first paint completes before loading the heavy shader background.

Comment on lines +156 to +162
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rendering of multiple mermaid diagrams was changed from parallel (Promise.all) to sequential (for...of with await). While this avoids potential mermaid concurrency issues (shared internal state), it means N diagrams now take N× the single-diagram time instead of roughly 1×.

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 render() truly cannot be called concurrently, adding a brief comment explaining this decision would help future maintainers understand the trade-off.

Comment thread pages/webpack.config.js
algorithm: 'brotliCompress',
compressionOptions: {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 11

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brotli compression at quality 11 (maximum) is extremely CPU-intensive and can significantly increase build times, especially for large bundles like three and mermaid. The marginal size reduction over quality 9 or 10 is typically negligible (<1-2%), while build time can increase by 5-10x. Consider using quality 9 or 10 for a much better build-time-to-compression-ratio tradeoff.

Suggestion:

Suggested change
[zlib.constants.BROTLI_PARAM_QUALITY]: 11
[zlib.constants.BROTLI_PARAM_QUALITY]: 9

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 with gzip and Brotli variants for production deployment.

Fixes alibaba#457
@lizhengfeng101
lizhengfeng101 force-pushed the perf/pages-code-splitting-457 branch from 86cdcea to b348686 Compare July 24, 2026 15:35
@lizhengfeng101

Copy link
Copy Markdown
Collaborator

Evaluation Results

I ran a local benchmark comparing main vs this PR under identical conditions (Python SimpleHTTPServer, no compression, Lighthouse Desktop preset, same machine).

Bundle Size

Metric Main PR Change
Entry Point (initial JS) 2,057 KiB 296 KiB -86%
Landing page total JS loaded 2,057 KiB 821 KiB -60%

Lighthouse Desktop

Metric Main PR Change
Performance 86 94 +8
FCP 0.6 s 0.8 s +0.2s (minor regression)
LCP 2.3 s 1.4 s -0.9s
TBT 20 ms 0 ms -20ms
Speed Index 1.7 s 1.4 s -0.3s
Transfer Size 2,293 KiB 1,057 KiB -54%

The code splitting and lazy-loading are working well. LCP and TBT improvements are significant.

Suggestion: Remove compression-webpack-plugin

The production site is served through GitHub Pages → Fastly → Alibaba Cloud CDN (Tengine), which already applies on-the-fly gzip (content-encoding: gzip confirmed in response headers). The pre-generated .gz/.br files from compression-webpack-plugin:

  1. Won't be served by GitHub Pages — it doesn't detect co-located .gz/.br files as compressed alternatives (that's an nginx gzip_static/brotli_static feature)
  2. Just increase the deployment artifact size — the files are uploaded but never requested by any client
  3. Are redundant — CDN compression handles this automatically

Recommend removing:

  • compression-webpack-plugin from package.json
  • require('zlib'), require('compression-webpack-plugin'), and both CompressionPlugin instances from webpack.config.js

The code splitting, lazy loading, and splitChunks configuration are all solid — only the pre-compression part is unnecessary for the current deployment architecture.

@lizhengfeng101

Copy link
Copy Markdown
Collaborator

Bug: Shader background visual regression

After deploying locally I noticed the green area in the hero section is noticeably more prominent than production. The root cause:

shaderFallback is always rendered, even after the shader loads, and ColorBends has transparent prop set — so the Three.js canvas has alpha transparency. The transparent regions of the shader reveal the radial-gradient fallback underneath, shifting the overall color balance toward green.

On main (no fallback div), the transparent parts of the shader show the black section background. On this PR, they show radial-gradient(circle at 50% 20%, #0d750d 0%, #042e04 38%, #000000 78%) instead.

Suggested fix

Hide the fallback once the shader has mounted:

{!showShaderBackground && shaderFallback}
{showShaderBackground && (
  <Suspense fallback={shaderFallback}>
    <ColorBends ... />
  </Suspense>
)}

This way:

  • Before shader loads: fallback gradient is visible (good, no black flash)
  • While shader chunk is loading (Suspense): fallback stays as the Suspense fallback
  • After shader mounts: fallback is removed from DOM, transparent shader regions show black as intended

This preserves the original visual appearance while keeping the loading UX smooth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(pages): enable webpack code splitting and lazy-load heavy dependencies

2 participants