Performance Deep-Dive: Why We Have No Code Splitting (And How We're Fixing It)
Performance Deep-Dive: Why We Have No Code Splitting (And How We're Fixing It)
Version: 1.0.22
Category: Performance / Bundle Optimization
The Problem
A recent audit of the entire src/ directory revealed a significant performance gap: there are zero dynamic imports anywhere in the codebase.
Every heavy client component — tabs, dashboards, settings panels, the command palette — is statically imported and bundled into the initial JavaScript payload. This means a user who visits the dashboard's Overview tab still pays the full parse-and-compile cost of the Beast Mode tab, the CRM Health dashboard, the Revenue dashboard, and every other tab they may never open during that session.
What's Being Loaded Upfront
The following components are currently eager-loaded on every dashboard page load:
| Component | Approximate Size | When Actually Needed |
|---|---|---|
BeastModeTab | 33 KB | Only when the Beast Mode tab is active |
ProjectFeaturesTab | 31 KB | Only when the Features tab is active |
SupportTicketsDashboard | 31 KB | Only when the Support tab is active |
CrmHealthDashboard | 29 KB | Only when the CRM tab is active |
EnvVarsSettings | 27 KB | Only when the Settings tab is active |
RevenueDashboard | 25 KB | Only when the Revenue tab is active |
CommandPalette | 18 KB | Only when the user presses Cmd+K |
The most egregious offender is CommandPalette: it is included in the bundle for every page across the entire application, yet it only renders when the user explicitly triggers it with a keyboard shortcut.
Why This Happens
The product detail page (src/app/dashboard/products/[id]/page.tsx) imports all tab components with standard static import statements at the top of the file:
// Current (problematic) pattern
import BeastModeTab from '@/components/beast-mode-tab';
import SupportTicketsDashboard from '@/components/support-tickets-dashboard';
import CrmHealthDashboard from '@/components/crm-health-dashboard';
import RevenueDashboard from '@/components/billing/revenue-dashboard';
// ... and so on
Next.js and the underlying webpack/Turbopack bundler treat these as hard dependencies and include them in the same chunk. Only one tab is visible at any given time, but all of them are parsed and compiled by the browser before the page becomes interactive.
The Performance Cost
- 150–250 KB of avoidable JavaScript is parsed on every dashboard page load.
- First Contentful Paint (FCP) is degraded by an estimated 300–600 ms on mobile devices and slower connections.
- Users on fast desktop connections may not feel this directly, but it compounds with other bundle weight and disproportionately affects mobile and emerging-market users.
The Fix: next/dynamic with ssr: false
The correct pattern for all of these components is to replace static imports with next/dynamic, which defers the download of each component's chunk until the moment it is actually rendered:
import dynamic from 'next/dynamic';
// Each component is now loaded on demand
const BeastModeTab = dynamic(
() => import('@/components/beast-mode-tab'),
{ ssr: false }
);
const ProjectFeaturesTab = dynamic(
() => import('@/components/project-features-tab'),
{ ssr: false }
);
const SupportTicketsDashboard = dynamic(
() => import('@/components/support-tickets-dashboard'),
{ ssr: false }
);
const CrmHealthDashboard = dynamic(
() => import('@/components/crm-health-dashboard'),
{ ssr: false }
);
const RevenueDashboard = dynamic(
() => import('@/components/billing/revenue-dashboard'),
{ ssr: false }
);
const EnvVarsSettings = dynamic(
() => import('@/components/env-vars-settings'),
{ ssr: false }
);
// Highest priority: loaded on every page, shown only on Cmd+K
const CommandPalette = dynamic(
() => import('@/components/command-palette'),
{ ssr: false }
);
Why ssr: false?
These are all heavy, interactive client components. They depend on browser APIs and do not meaningfully benefit from server-side rendering. Disabling SSR for them prevents Next.js from attempting to hydrate them server-side, which would be wasted work.
Loading States
When using next/dynamic, you can supply a lightweight loading placeholder that displays while the chunk fetches:
const BeastModeTab = dynamic(
() => import('@/components/beast-mode-tab'),
{
ssr: false,
loading: () => <div className="animate-pulse h-64 bg-muted rounded-lg" />,
}
);
This provides a smooth user experience — the tab skeleton appears instantly while the real component loads in the background.
Priority Order for Implementation
CommandPalette— highest leverage; affects every page, not just the product detail page.BeastModeTab— largest component at 33 KB.ProjectFeaturesTabandSupportTicketsDashboard— both 31 KB, equally high impact.CrmHealthDashboard,EnvVarsSettings,RevenueDashboard— remaining tab components.
Expected Outcome
Once all seven components are lazy-loaded:
- The initial JS payload for dashboard pages drops by an estimated 150–250 KB.
- FCP improves by 300–600 ms on mobile and slower connections.
- Bundle chunks are only fetched when a user actually navigates to a tab or triggers the command palette, reducing bandwidth consumption for users who never visit certain sections.
- The pattern establishes a standard for all future heavy components added to the platform.
Tracked in release v1.0.22. Implementation is prioritized in the upcoming sprint.