Split by route first, then by interaction, and enforce size budgets in CI.
By route: users only need the current page. Frameworks like Next.js do this automatically.
By interaction: heavy components that are not needed for the first paint load later. They load with import() / React.lazy when they are shown, or when the user hovers the trigger. Examples:
- a rich text editor
- charts
- a map
- a modal's contents
A large vendor library that only one feature uses stays in that feature's chunk.
I avoid over-splitting into hundreds of tiny chunks. That adds request overhead and waterfalls. A waterfall is a chain of requests where each one waits for the one before. Shared code ends up in common chunks, so the browser caches it across routes.
Budgets: I set limits per route on JavaScript transferred. For example, 170 KB compressed for the initial route. CI (continuous integration: the automatic checks on each pull request) enforces them with a size-check tool. The tool fails the PR or comments the size difference. A bundle analyser in CI shows what grew.
I also watch dependency choices. Date and utility libraries are frequent causes of growth. Tree-shakeable ESM imports matter too. Tree-shaking means the bundler drops code you never import.
- Split by route first.
- Lazy load heavy features that are below the fold or only needed after an interaction.
- Avoid over-splitting, which causes request overhead and waterfalls.
- Set size budgets per route and enforce them in CI.
- Use bundle analysis and review new dependencies.
- patterns.dev (https://www.patterns.dev/)patterns.dev