Skip to content

Commit 11bc78c

Browse files
perf: bundle optimization with code splitting and vendor chunking (#4709)
## Summary - **Route-level code splitting**: Lazy-load 5 pages (CatalogPage, LegalPage, McpPage, InteractivePage, DebugPage) via React Router `lazy()` - **PrismLight**: Switch from full Prism (37+ languages) to `prism-light` with only Python registered, removing ~36 unused language grammars - **Vendor chunk splitting**: Split React/MUI into separate cached chunks via `manualChunks` — users only re-download app code on updates - **API preconnect**: Add `<link rel="preconnect">` for `api.pyplots.ai` to reduce initial API latency ### Build output (before → after) | Chunk | Size | Gzipped | |-------|------|---------| | `vendor-*.js` | 98 KB | 33 KB | | `mui-*.js` | 320 KB | 99 KB | | `index-*.js` | 344 KB | 108 KB | | Lazy pages | 3-11 KB each | 1-3 KB each | Main bundle reduced from ~1.3 MB to 344 KB with vendor/MUI cached separately. ## Test plan - [x] `yarn build` succeeds with expected chunk split - [x] All 44 tests pass - [ ] Verify syntax highlighting works on spec pages (Code tab) - [ ] Verify lazy-loaded pages load correctly (catalog, legal, mcp, debug, interactive) Closes #4704 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 393f243 commit 11bc78c

File tree

4 files changed

+29
-11
lines changed

4 files changed

+29
-11
lines changed

app/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
<meta name="twitter:image" content="https://pyplots.ai/og-image.png" />
2424
<!-- Preconnect to GCS for font loading -->
2525
<link rel="preconnect" href="https://storage.googleapis.com" crossorigin>
26+
<!-- Preconnect to API for faster first request -->
27+
<link rel="preconnect" href="https://api.pyplots.ai" crossorigin>
2628

2729
<!-- Preload MonoLisa Basic Latin (most used, variable font = all weights) -->
2830
<link rel="preload" href="https://storage.googleapis.com/pyplots-static/fonts/0-MonoLisa-normal.woff2" as="font" type="font/woff2" crossorigin>

app/src/components/SpecTabs.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
1515
import CheckIcon from '@mui/icons-material/Check';
1616
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
1717
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
18-
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
18+
import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light';
1919
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
20+
import python from 'react-syntax-highlighter/dist/esm/languages/prism/python';
21+
22+
SyntaxHighlighter.registerLanguage('python', python);
2023

2124
// Map tag category names to URL parameter names
2225
const SPEC_TAG_PARAM_MAP: Record<string, string> = {

app/src/router.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,37 @@
11
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
22
import { HelmetProvider } from 'react-helmet-async';
3+
import Box from '@mui/material/Box';
4+
import CircularProgress from '@mui/material/CircularProgress';
35
import { Layout, AppDataProvider } from './components/Layout';
46
import { ErrorBoundary } from './components/ErrorBoundary';
57
import { HomePage } from './pages/HomePage';
68
import { SpecPage } from './pages/SpecPage';
7-
import { CatalogPage } from './pages/CatalogPage';
8-
import { InteractivePage } from './pages/InteractivePage';
9-
import { DebugPage } from './pages/DebugPage';
10-
import { LegalPage } from './pages/LegalPage';
11-
import { McpPage } from './pages/McpPage';
129
import { NotFoundPage } from './pages/NotFoundPage';
1310

11+
const LazyFallback = () => (
12+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
13+
<CircularProgress size={32} />
14+
</Box>
15+
);
16+
1417
const router = createBrowserRouter([
1518
{
1619
path: '/',
1720
element: <Layout />,
1821
children: [
1922
{ index: true, element: <HomePage /> },
20-
{ path: 'catalog', element: <CatalogPage /> },
21-
{ path: 'legal', element: <LegalPage /> },
22-
{ path: 'mcp', element: <McpPage /> },
23+
{ path: 'catalog', lazy: () => import('./pages/CatalogPage').then(m => ({ Component: m.CatalogPage, HydrateFallback: LazyFallback })) },
24+
{ path: 'legal', lazy: () => import('./pages/LegalPage').then(m => ({ Component: m.LegalPage, HydrateFallback: LazyFallback })) },
25+
{ path: 'mcp', lazy: () => import('./pages/McpPage').then(m => ({ Component: m.McpPage, HydrateFallback: LazyFallback })) },
2326
{ path: ':specId', element: <SpecPage /> },
2427
{ path: ':specId/:library', element: <SpecPage /> },
2528
{ path: '*', element: <NotFoundPage /> },
2629
],
2730
},
2831
// Fullscreen interactive view (outside Layout but inside AppDataProvider)
29-
{ path: 'interactive/:specId/:library', element: <InteractivePage /> },
32+
{ path: 'interactive/:specId/:library', lazy: () => import('./pages/InteractivePage').then(m => ({ Component: m.InteractivePage, HydrateFallback: LazyFallback })) },
3033
// Hidden debug dashboard (outside Layout - no header/footer)
31-
{ path: 'debug', element: <DebugPage /> },
34+
{ path: 'debug', lazy: () => import('./pages/DebugPage').then(m => ({ Component: m.DebugPage, HydrateFallback: LazyFallback })) },
3235
{ path: '*', element: <NotFoundPage /> },
3336
]);
3437

app/vite.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,14 @@ export default defineConfig({
1111
port: 3000,
1212
host: true,
1313
},
14+
build: {
15+
rollupOptions: {
16+
output: {
17+
manualChunks(id) {
18+
if (id.includes('node_modules/@mui/')) return 'mui';
19+
if (id.includes('node_modules/react/') || id.includes('node_modules/react-dom/') || id.includes('node_modules/react-router-dom/')) return 'vendor';
20+
},
21+
},
22+
},
23+
},
1424
});

0 commit comments

Comments
 (0)