Code Splitting
Code Splitting is the technique of dividing a JavaScript bundle into multiple chunks during the build process and loading each chunk at runtime only when it is needed. Instead of forcing every user to download the entire application upfront, only the code required for the current page or feature is fetched, reducing initial load time and improving perceived performance.
βΆArchitecture Diagram
π StructureDashed line animations indicate the flow direction of data or requests
An SPA compiles the entire application into a single bundle that is downloaded all at once on the first visit. As the app grows, so does this bundle, meaning a user who only wants to see the first page must still download the code for signup, admin, and analytics dashboards. When the bundle exceeds hundreds of kilobytes, JavaScript parsing and execution time increases, and on slow networks a blank screen can persist for several seconds. Every feature addition slows down the initial load for all users.
In the early web, each page loaded its own HTML and scripts separately, so bundle size was not an issue. When SPA frameworks like React and Angular arrived and the client took on the entire app logic, bundle size naturally became a performance bottleneck. Initially, vendor chunk separation was the main response, but as apps grew larger, a pattern of downloading only the necessary code per route or feature at the moment it was needed emerged. After webpack's support for dynamic import() and ECMAScript standardization (2020), this pattern became the default optimization approach across the ecosystem.
Code Splitting splits the bundle into multiple chunks during the build and loads each at runtime when needed. When the build tool encounters a dynamic import like `import('./SomeComponent')`, it separates that module and its dependencies into a distinct chunk file. The build output consists of an Initial Chunk that is essential for the first page load, and Lazy Chunks that are fetched later on demand. When the browser enters the relevant route or uses a specific feature, the JavaScript runtime issues a network request for that chunk's URL, downloads it, and executes it. Already-downloaded chunks remain in the browser cache and are not re-fetched on subsequent visits. In React, combining `React.lazy()` with `Suspense` allows this pattern to be used declaratively at the component level. Suspense handles the fallback UI shown during loading, avoiding a blank screen.
Code Splitting and Tree Shaking both reduce bundle size but take different approaches. Tree Shaking statically analyzes the code at build time and removes code that is never actually used -- the resulting bundle is one file, but with unused code stripped out. Code Splitting is not about removing code but dividing it. It defers code that the app genuinely needs but does not need right now to be fetched later. The two techniques are typically used in tandem. When used with Service Workers, strategies must be aligned. Pre-caching chunks via a Service Worker can serve Lazy Chunks instantly even offline, but failing to scope which chunks are pre-cached can bloat the cache unnecessarily. If offline support is the goal, Code Splitting and Service Worker caching strategies should be designed together. There are also situations where Code Splitting is not appropriate. If the app itself is small or most code is needed on the first page, splitting chunks only adds network round trips and can make things slower.
The more pages an app has, the greater the benefit of route-based splitting. Lightweight pages like the main page, login, and product detail load fast with just the Initial Chunk, while heavy features like analytics dashboards or text editors have their cost borne only by users who actually visit those pages. The signal to consider adopting Code Splitting in practice is when Lighthouse or a bundle analyzer shows the bundle size exceeding a certain threshold, or when First Contentful Paint or Time to Interactive fall outside target ranges. The starting point is using a bundle analyzer to identify which modules are largest, then checking whether those modules are truly needed on the first screen. However, splitting too finely increases the number of chunks and network requests, which can backfire. The appropriate chunk granularity is around routes or meaningful feature groups.