Code Splitting for Better SEO Performance: How We Reduced Bundle Size by 73% and Improved Core Web Vitals
How strategic code splitting reduced our initial bundle from 847KB to 228KB, improved Largest Contentful Paint by 61%, and boosted our Google rankings without changing a single line of application logic.
How strategic code splitting reduced our initial bundle from 847KB to 228KB, improved Largest Contentful Paint by 61%, and boosted our Google rankings without changing a single line of application logic.
The Problem: Excellent Content, Terrible Core Web Vitals
When we launched FIKR's content hub in early 2024, we were proud of the engineering quality. Our React + Vite setup was modern, our components were well-architected, and our features were impressive. We had everything from interactive calculators to real-time collaboration tools, all beautifully designed and thoroughly tested.
Then we checked Google Search Console. Our Core Web Vitals were a disaster. Pages that should have loaded in under a second were taking 3.8 seconds to become interactive. Our Largest Contentful Paint (LCP) averaged 4.2 seconds. Time to Interactive (TTI) was pushing 5 seconds on mobile devices. Google's PageSpeed Insights gave us a dismal performance score of 34 out of 100.
The root cause was obvious but embarrassing: our initial JavaScript bundle was 847KB. Every visitor, regardless of which page they landed on, had to download code for features they'd never use. Someone reading a blog post about fundraising was downloading our entire equity calculator. A visitor on our pricing page was loading our complete charting library. We were shipping an entire application when most users needed just a slice.
What made it worse was knowing this was entirely preventable. Modern bundlers like Webpack and Vite have sophisticated code-splitting capabilities built in. We just weren't using them effectively. The solution wasn't to rebuild our application or switch frameworks—it was to restructure how we shipped the code we'd already written. We needed a systematic approach to code splitting that would work with our existing architecture and could scale as we added more features.
THE COMPLETE SYSTEM TO SCALE YOUR STARTUP
From strategic planning to execution, FIKR gives you every tool needed to grow from idea to exit.
START FOR FREE →No credit card required • Setup in 5 minutes
Understanding Strategic Code Splitting
Code splitting is the practice of breaking your JavaScript bundle into smaller chunks that can be loaded on demand, rather than forcing users to download everything upfront. The key word here is "strategic"—not all code splitting improves performance. Done poorly, it can actually make things worse by creating waterfalls of dependent requests.
The fundamental principle is this: identify the critical path for each page type, load only what's necessary for that path, and defer everything else until it's actually needed. For a blog post, the critical path is the content itself—the text, images, and basic formatting. Interactive widgets, comment systems, and related article carousels can wait. For a dashboard, the critical path is the main data visualization—detailed filters and export tools can load later.
Here's what happens when a user visits a code-split application:
- Initial Request: The browser downloads a minimal HTML file with inline critical CSS and a tiny JavaScript bootstrap (typically 15-30KB).
- Route Detection: The bootstrap determines which page the user is viewing and requests only the chunks needed for that specific route.
- Parallel Loading: The browser fetches the route-specific code and any shared dependencies in parallel, taking advantage of HTTP/2 multiplexing.
- Progressive Enhancement: As chunks load, features become available progressively. The page is usable before everything has downloaded.
- Lazy Loading: When the user interacts with features that require additional code (opening a modal, clicking an advanced filter), those chunks are fetched on demand.
This approach transforms the user experience. Instead of a 3-second blank screen followed by a fully interactive page, users see content in 400ms and can start reading while secondary features load in the background. The perceived performance improvement is dramatic, even if the total download size hasn't changed significantly.
Why We Didn't Choose Full Server-Side Rendering
When facing performance problems, the obvious solution seems to be server-side rendering (SSR). Frameworks like Next.js and Remix make SSR relatively straightforward, and it solves the initial load problem by sending fully rendered HTML to the browser. We seriously considered migrating to Next.js.
We ultimately decided against it for our specific context. We're building a multi-tenant SaaS platform with dozens of interactive features. Full SSR would have been a major architectural change requiring months of work. Here are the key factors that made us hesitant:
- Migration Cost: We estimated 8-12 weeks to migrate our existing React + Vite codebase to Next.js, rewrite dozens of components to work with SSR, and handle hydration edge cases. That timeline didn't include fixing the inevitable bugs that would surface in production.
- Server Infrastructure: SSR requires running Node.js servers, which adds operational complexity and cost. Our current setup serves static assets from Cloudflare's edge network for ~$50/month. SSR would have required Vercel or similar at $200-400/month minimum, plus database costs for server-side data fetching.
- Interactive Features: Much of our value comes from real-time collaboration, live data updates, and rich interactions. These features require client-side JavaScript regardless of how the initial HTML is rendered. SSR would improve the first paint but wouldn't eliminate the JavaScript bundle size problem.
- Caching Complexity: Our content is user-specific and organization-specific. Implementing effective caching strategies for SSR with this level of personalization is challenging. We'd need sophisticated cache invalidation and edge caching logic.
- Development Velocity: Our team is comfortable with client-side rendering patterns. Adding SSR would slow down feature development as engineers navigate hydration issues, server/client environment differences, and Next.js-specific patterns.
SSR is excellent for content-heavy sites with relatively static content and simpler interactions. For our use case—a feature-rich SaaS application—strategic code splitting offered better ROI. We could implement it incrementally, our infrastructure costs stayed low, and we maintained our development velocity while dramatically improving performance.
THE COMPLETE SYSTEM TO SCALE YOUR STARTUP
From strategic planning to execution, FIKR gives you every tool needed to grow from idea to exit.
START FOR FREE →No credit card required • Setup in 5 minutes
Implementation: Route-Based Code Splitting
We implemented code splitting in three layers: routes, features, and heavy dependencies. The foundation was route-based splitting using React's lazy loading and Vite's automatic code splitting. This ensures that each major section of the application—marketing pages, blog, dashboard, admin—loads only its own code.
Route-Based Splitting with React Router:
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import LoadingSpinner from './components/LoadingSpinner';
// Lazy load route components
const HomePage = lazy(() => import('./pages/HomePage'));
const BlogPost = lazy(() => import('./pages/BlogPost'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const PricingPage = lazy(() => import('./pages/PricingPage'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/blog/:slug" element={<BlogPost />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/pricing" element={<PricingPage />} />
</Routes>
</Suspense>
);
}
This single change reduced our initial bundle size from 847KB to 412KB—a 51% reduction. Each route now loads only its specific code. The blog route doesn't include dashboard components, and vice versa. Vite automatically generates separate chunks for each lazy-loaded component and handles the dynamic imports.
Feature-Based Lazy Loading:
import { lazy, Suspense, useState } from 'react';
// Heavy components loaded only when needed
const EquityCalculator = lazy(() => import('./components/EquityCalculator'));
const ChartingLibrary = lazy(() => import('./components/ChartingLibrary'));
const AdvancedFilters = lazy(() => import('./components/AdvancedFilters'));
function Dashboard() {
const [showCalculator, setShowCalculator] = useState(false);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setShowCalculator(true)}>
Open Calculator
</button>
{showCalculator && (
<Suspense fallback={<div>Loading calculator...</div>}>
<EquityCalculator />
</Suspense>
)}
</div>
);
}
Feature-based lazy loading ensures that heavy, interactive components are downloaded only when users actually need them. The equity calculator, which includes complex financial math libraries, is 143KB minified. Most users never open it, so there's no reason to include it in the initial bundle. When they click the button, the chunk downloads in 180ms on a typical connection—fast enough that the loading state is barely visible.
Optimizing Vite Bundle Configuration:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor splitting for better caching
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
'chart-vendor': ['recharts', 'd3'],
'editor-vendor': ['@tiptap/react', '@tiptap/starter-kit'],
},
},
},
// Reduce chunk size threshold to create more granular splits
chunkSizeWarningLimit: 500,
},
});
The manual chunk configuration separates vendor code from application code and groups related libraries together. This improves caching—when we update application code, the vendor chunks remain unchanged, and returning visitors don't redownload them. The chunk size warning at 500KB forces us to be conscious about bundle growth and split large chunks further.
Real Results After 3 Weeks
We implemented code splitting across our entire application over three weeks: one week for route splitting, one week for feature-based lazy loading, and one week for optimization and testing. The performance improvements exceeded our expectations.
Before Code Splitting:
- Initial Bundle Size: 847KB (minified, gzipped)
- Largest Contentful Paint: 4.2 seconds average
- Time to Interactive: 5.1 seconds average
- PageSpeed Score: 34/100 mobile, 58/100 desktop
- Core Web Vitals Status: Failing (red) on all metrics
After Code Splitting (3 Weeks):
- Initial Bundle Size: 228KB (73% reduction)
- Largest Contentful Paint: 1.6 seconds average (61% improvement)
- Time to Interactive: 2.1 seconds average (59% improvement)
- PageSpeed Score: 87/100 mobile, 94/100 desktop
- Core Web Vitals Status: Passing (green) on all metrics
- Infrastructure Cost: No change, still $50/month
The SEO impact was equally dramatic. Within two weeks of deploying code splitting, we saw a 23% increase in organic traffic. Our average position in Google search results improved by 8 positions for our primary keywords. Google Search Console confirmed that our pages moved from "Poor" to "Good" URL status for Core Web Vitals, and we started appearing in more featured snippets and answer boxes.
Challenges We Had to Overcome
Implementing code splitting wasn't without obstacles. The biggest challenge was handling loading states gracefully. When every feature can load asynchronously, you need a consistent loading experience that doesn't feel jarring or broken.
We solved this through a component pattern strategy:
- Skeleton Screens: Instead of generic spinners, we created skeleton components that match the shape of the loading content. Users see a gray outline of the chart while it loads, which feels faster than a blank space.
- Preloading on Hover: For features behind buttons or tabs, we preload the chunks when users hover over the trigger. By the time they click, the code is already downloaded. This reduced perceived loading time by 70%.
- Error Boundaries: We wrapped all lazy-loaded components in error boundaries that gracefully handle chunk loading failures. Network issues or cache problems don't crash the entire application—they show a retry button instead.
- Prefetch Links: For common user flows, we add prefetch link tags to the HTML. When a user lands on the homepage, we prefetch the pricing page chunk because 60% of users navigate there next.
- Progressive Enhancement: Core functionality works even if JavaScript fails to load or is blocked. Content is readable, forms are submittable, and navigation works. Enhanced interactions load progressively as chunks arrive.
The second major challenge was testing. With code scattered across dozens of chunks, we needed to ensure that everything loaded correctly in production. We addressed this through comprehensive monitoring:
- Bundle Analysis: We run Webpack Bundle Analyzer on every build to visualize chunk sizes and catch regressions. If a chunk grows above our threshold, the CI build fails.
- Real User Monitoring: We track chunk loading times, failure rates, and cache hit rates using browser performance APIs. This catches issues like chunks failing to load on specific browsers or networks.
- Synthetic Monitoring: Every 5 minutes, we test critical user flows from different geographic regions to ensure chunks are accessible and performant globally.
- Result: Chunk loading errors dropped to 0.04% of page loads, and we catch bundle size regressions before they reach production.
When Code Splitting Makes Sense vs Full SSR
After implementing both approaches in different projects, we've developed a clear framework for choosing between aggressive code splitting and server-side rendering.
Choose Code Splitting if:
- You have a feature-rich SaaS application with many interactive elements
- Your content is highly personalized or user-specific
- You want to keep infrastructure simple and costs low
- Your team is comfortable with client-side rendering patterns
- You need to implement performance improvements incrementally
- Your application has distinct sections that users rarely cross in a single session
Choose Server-Side Rendering if:
- You're building a content-heavy site (blog, documentation, e-commerce)
- Your content is mostly static or can be cached effectively
- SEO is critical and you need perfect social media previews
- You're starting a new project (easier than migrating existing apps)
- You have budget for managed hosting platforms like Vercel or Netlify
- Your team is willing to learn SSR-specific patterns and handle hydration complexity
Final Thoughts
Code splitting transformed our application's performance without requiring a major architectural change. The 73% reduction in initial bundle size and 61% improvement in Largest Contentful Paint directly translated to better Core Web Vitals scores and increased organic traffic. More importantly, the user experience improved dramatically—pages feel fast and responsive.
What surprised us most was how little we had to change about our application logic. We didn't rewrite components or redesign features. We simply reorganized how we delivered the code to users' browsers. The tools for effective code splitting—React.lazy, dynamic imports, Vite's automatic chunking—are built into the frameworks most teams already use.
The implementation took three weeks from start to finish. One engineer led the effort, with occasional input from the team on testing and rollout strategy. We deployed incrementally, starting with route-based splitting, then adding feature-based lazy loading, and finally optimizing vendor chunks. Each phase delivered measurable performance improvements.
If you're struggling with Core Web Vitals or PageSpeed scores, audit your bundle size before considering major architectural changes. There's a good chance you can achieve substantial improvements through strategic code splitting. Start with route-based splits to get quick wins, then identify your heaviest features and make them lazy loadable. The ROI is excellent—better SEO, happier users, and no infrastructure changes required.
THE COMPLETE SYSTEM TO SCALE YOUR STARTUP
From strategic planning to execution, FIKR gives you every tool needed to grow from idea to exit.
START FOR FREE →No credit card required • Setup in 5 minutes