2026-09-13 · 6 min · By Alcott Dube
How to reduce JavaScript bundle size as a project grows
I keep growing web projects fast by budgeting initial JavaScript, splitting optional features, and checking browser work rather than trusting a smaller output folder.

I reduce JavaScript bundle size by setting per-route transfer budgets, removing unnecessary dependencies, and loading optional features only when they're needed. I check production builds on a slower device as well as inspecting compressed file sizes, because smaller downloads don't guarantee less browser work.
How to measure JavaScript bundle size in production
I start with a production build, not the development server. Vite's development experience and its production output serve different purposes, so a quick local reload tells me little about delivery cost. I build the site, serve the output locally, and inspect the browser's network and performance panels. Before comparing results, I check that the server is actually compressing responses.
For each important route, I record compressed JavaScript transferred on a cold visit, uncompressed resource size, and time spent evaluating scripts. These answer different questions: how much crosses the network, how much code arrives after decompression, and how much browser work follows. I include shared chunks and third-party scripts, not just the file carrying the route's name.
I choose three representative paths: the main landing page, a common task, and the heaviest feature. I test direct visits and navigation between them. A warm cache can hide an expensive first visit; a direct visit can miss code fetched during interaction. I keep the device, throttling settings, and test sequence fixed so comparisons mean something. Build duration gets a separate record, since it measures developer waiting time rather than visitor experience.
How to set a JavaScript bundle budget
I make the budget a release constraint, not a number buried in a report. web.dev's performance-budget guidance treats limits as a way to make trade-offs explicit. That matters when a dependency looks harmless in isolation but becomes the fourth addition to an already heavy page. The limit needs an owner and an agreed response when a change exceeds it.
For a small content site, I might propose the following starting limits. These are example project decisions, not universal targets or figures taken from research. I'd adjust them after testing the actual content, devices, and features.
- Keep first-visit JavaScript below 170 kB compressed per core route, including shared chunks.
- Flag any increase above 10 kB compressed on a core route for review.
- Give an optional editor a separate 120 kB compressed allowance, loaded only after it is requested.

How to find dependencies that increase bundle size
I inspect a bundle breakdown before changing imports at random. The useful question is which dependency is present on which route, and why. A charting package on a reporting screen may be justified. The same package on a public landing page usually points to an import boundary problem. I trace that path back to the importing module.
I look first for duplicate libraries, unused locale data, large utilities used for one small operation, and packages imported through a shared component. A date formatter, icon collection, or document exporter can become everyone's problem when it sits behind an application-wide import. I also check whether two versions of one dependency are being shipped through different parents. Removing duplication beats compressing it twice.
Then I choose between removal, replacement, narrower imports, or deferral. I don't assume named imports guarantee a small result: elimination of unused code depends on package structure, side effects, and the build tool's analysis. I check the output after each change. Replacing a mature dependency with home-written parsing or validation code can exchange a measurable download cost for a less visible correctness cost.
How to split code by route and feature in Vite
I split at boundaries that match actual use. Routes are a sensible starting point; expensive optional features are the next. web.dev's code-splitting guidance describes loading code when it becomes necessary rather than sending everything upfront. In practice, I'd keep a document exporter out of the initial route and request its module when someone chooses to export.
A dynamic import creates an asynchronous boundary that Vite can use in its production build. I still inspect the dependency graph: if another part of the application imports the same module eagerly, the intended saving may disappear. Vite's documentation also explains its preload optimisation for shared dependencies of asynchronous chunks. That helps loading, but doesn't make unnecessary code free.
I avoid splitting every small component. Too many tiny chunks add request overhead and can create loading chains. I also avoid deferring code needed to render the page's main content, since that can delay the useful screen. For an optional feature, I provide a loading state and a retry path if the chunk request fails. A smaller entry file isn't a win if the first click becomes unreliable.
What to measure after reducing JavaScript
After a size reduction, I repeat the same user task and inspect the performance trace. I want to know whether script evaluation fell, whether the main thread became available sooner, and whether interaction delays improved. web.dev identifies tasks longer than 50 milliseconds as long tasks. I use that threshold to find blocking work, not as proof that everything shorter is harmless.
I watch largest contentful paint for loading and interaction to next paint for responsiveness, alongside the trace. Neither metric belongs exclusively to JavaScript. A slow image, server response, or expensive layout can dominate the result. That is why I won't claim a bundle change fixed the page merely because one lab run returned a better score.
I run several comparable lab tests and examine the spread rather than selecting the fastest result. When field measurements are available, I check the 75th percentile and segment by route and device category where sample sizes allow. I also test the deferred feature itself. Moving 200 milliseconds of work from page load to a button click has changed the timing, not necessarily the experience.
How to stop JavaScript bundle size growing again
I put the size check into the automated build and compare each change with the main branch. The report needs route totals, not only individual chunk sizes, because files can move around without reducing what a visitor downloads. I count each required chunk once per route and keep compression settings consistent. Otherwise, a tooling change can look like a product improvement.
When a limit is exceeded, I require a decision: remove something, defer the addition, or approve a revised budget with a reason. For example, an accessible editor may justify more code than a plain text field. The trade-off should name the affected route and expected user benefit. A permanent exception with no owner is simply an abandoned budget.
I skip arbitrary chunk reshuffling unless measurement shows a loading or caching problem. Manual chunk rules can become another dependency map to maintain as the project changes. I would rather spend that maintenance effort reviewing new dependencies and preserving clear feature boundaries. After a major dependency upgrade, I rerun both cold and warm navigation tests; a familiar package name doesn't guarantee familiar output.
Questions people ask
What is a good JavaScript bundle size?
I don't use one limit for every product. I set a compressed, per-route budget against target devices and tasks, then check browser execution time. A content page and a browser-based editor need different allowances.
Does code splitting reduce total JavaScript size?
Not necessarily. I use it to reduce what a visitor downloads before a particular task. Total output can stay similar or grow slightly, while visitors who never open optional features download less.
Does Vite remove unused JavaScript automatically?
Vite's production build can eliminate unused code where static analysis and package structure allow it. I still check the output, because side effects and dependency packaging can prevent the removal I expected.
Should I measure gzip or Brotli bundle size?
I measure the encoding the production server actually delivers. For automated comparisons, I keep the compression method and settings fixed. I also track uncompressed size and execution time, since compression doesn't remove browser work.