From fd1df93a8fa320800275c135d5dd89390eb72c19 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Sat, 27 Dec 2025 08:02:08 -0600 Subject: [PATCH] perf: reduce practice page dev bundle from 47MB to 115KB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dynamic import echarts-for-react in SkillProgressChart, ValidationCharts, and SkillDifficultyCharts to avoid bundling 58MB echarts library - Lazy load game-registry in CreateRoomModal to prevent all arcade games from being bundled into every page using PageWithNav The practice page was bundling echarts (58MB) and all arcade game code because of static imports in shared components. These changes ensure: - echarts only loads when charts are actually rendered - game-registry only loads when CreateRoomModal is opened Bundle size: 47MB → 115KB (99.8% reduction) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/components/blog/SkillDifficultyCharts.tsx | 8 +++++++- apps/web/src/components/blog/ValidationCharts.tsx | 8 +++++++- apps/web/src/components/nav/CreateRoomModal.tsx | 15 ++++++++++++--- .../components/practice/SkillProgressChart.tsx | 8 +++++++- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/blog/SkillDifficultyCharts.tsx b/apps/web/src/components/blog/SkillDifficultyCharts.tsx index d67dd476..02b0e64e 100644 --- a/apps/web/src/components/blog/SkillDifficultyCharts.tsx +++ b/apps/web/src/components/blog/SkillDifficultyCharts.tsx @@ -1,8 +1,14 @@ 'use client' +import dynamic from 'next/dynamic' import { useState, useEffect } from 'react' -import ReactECharts from 'echarts-for-react' import * as Tabs from '@radix-ui/react-tabs' + +// Dynamic import echarts to reduce bundle size +const ReactECharts = dynamic(() => import('echarts-for-react'), { + ssr: false, + loading: () =>
Loading chart...
, +}) import { css } from '../../../styled-system/css' interface SkillData { diff --git a/apps/web/src/components/blog/ValidationCharts.tsx b/apps/web/src/components/blog/ValidationCharts.tsx index 68b6b144..09a19fad 100644 --- a/apps/web/src/components/blog/ValidationCharts.tsx +++ b/apps/web/src/components/blog/ValidationCharts.tsx @@ -1,8 +1,14 @@ 'use client' import * as Tabs from '@radix-ui/react-tabs' -import ReactECharts from 'echarts-for-react' +import dynamic from 'next/dynamic' import { useEffect, useState } from 'react' + +// Dynamic import echarts to reduce bundle size +const ReactECharts = dynamic(() => import('echarts-for-react'), { + ssr: false, + loading: () =>
Loading chart...
, +}) import { css } from '../../../styled-system/css' const chartContainerStyles = css({ diff --git a/apps/web/src/components/nav/CreateRoomModal.tsx b/apps/web/src/components/nav/CreateRoomModal.tsx index a1a25f84..d71c1051 100644 --- a/apps/web/src/components/nav/CreateRoomModal.tsx +++ b/apps/web/src/components/nav/CreateRoomModal.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import * as Select from '@radix-ui/react-select' import { animated } from '@react-spring/web' import { Modal } from '@/components/common/Modal' import { useCreateRoom, useRoomData } from '@/hooks/useRoomData' -import { getAvailableGames } from '@/lib/arcade/game-registry' import { RoomShareButtons } from './RoomShareButtons' +import type { GameDefinition } from '@/lib/arcade/game-sdk/types' export interface CreateRoomModalProps { /** @@ -31,8 +31,17 @@ type ModalState = 'creating' | 'created' export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalProps) { const { mutateAsync: createRoom, isPending } = useCreateRoom() const { getRoomShareUrl } = useRoomData() - const availableGames = getAvailableGames() + const [availableGames, setAvailableGames] = useState[]>([]) const [error, setError] = useState('') + + // Lazy load game registry only when modal opens + useEffect(() => { + if (isOpen && availableGames.length === 0) { + import('@/lib/arcade/game-registry').then(({ getAvailableGames }) => { + setAvailableGames(getAvailableGames()) + }) + } + }, [isOpen, availableGames.length]) const [gameName, setGameName] = useState('__choose_later__') // Special value = user will choose later const [accessMode, setAccessMode] = useState< 'open' | 'password' | 'approval-only' | 'restricted' diff --git a/apps/web/src/components/practice/SkillProgressChart.tsx b/apps/web/src/components/practice/SkillProgressChart.tsx index 08c47343..09bc74c1 100644 --- a/apps/web/src/components/practice/SkillProgressChart.tsx +++ b/apps/web/src/components/practice/SkillProgressChart.tsx @@ -1,7 +1,13 @@ 'use client' -import ReactECharts from 'echarts-for-react' +import dynamic from 'next/dynamic' import { useCallback, useMemo, useState } from 'react' + +// Dynamic import echarts to avoid bundling 58MB library on pages that don't use charts +const ReactECharts = dynamic(() => import('echarts-for-react'), { + ssr: false, + loading: () =>
Loading chart...
, +}) import { getExtendedClassification, type ExtendedSkillClassification,