JavaScript bundle size and code splitting
Large bundles delay loading and block taps. How to see what is in yours, remove what is not needed, and split the rest so each page loads only its own code.
In this guide
Why bundle size matters
web.dev puts the cost plainly: even unused JavaScript has to be downloaded, parsed, compiled and then executed. That adds delay in loading, in memory and in main-thread activity, and the main thread is the one that must answer taps and typing. Shipping less code at startup helps the page become interactive sooner. It can help Largest Contentful Paint too, when the main content is drawn by JavaScript, because a smaller bundle arrives and runs sooner. See where main-thread time goes.
How big is the problem? In the 2025 Web Almanac the median mobile home page loaded 632 KB of JavaScript. Those are bytes over the wire, after compression. The same chapter measured about 251 KB of unused JavaScript on the median mobile page, counted after the files were unpacked, and it notes that unpacked sizes can be several times larger than the transferred ones. The typical single file is small, around 5 KB, so the weight comes from how many files there are and from a few large ones.
Step 1: see what is in the bundle
You cannot cut what you cannot see.
- Chrome DevTools. The Network panel shows the size of every file. The Coverage tool shows how much of each JavaScript file ran during your recording. The unused code guide shows the steps.
- Lighthouse. The diagnostics Reduce unused JavaScript and Reduce JavaScript execution time, and the Legacy JavaScript and Duplicated JavaScript insights, name the files that are worth a look.
- A bundle visualizer. web.dev recommends the Webpack Bundle Analyzer for projects built with webpack, and notes that other bundlers such as Parcel and Rollup have visualization tools too. They draw a zoomable treemap of everything inside the bundle, which makes a surprise easy to spot, such as a library bundled with all of its languages or two copies of one package.
- Source maps. Keep them available. Tools, and Lighthouse's own legacy-code check, use them to say which package a piece of code came from.
Step 2: remove what you do not need
- Delete libraries nobody uses. They are often left over from a feature that no longer exists.
- Import only what you use. web.dev's example is a project that needed only the database part of a large package and changed
import firebase from 'firebase'to importing just the app and database parts. Many libraries offer smaller entry points or function-by-function imports. - Rely on tree shaking. MDN describes tree shaking as the removal of dead code. It depends on
importandexport, so it works when the library ships as ES modules and you import named functions. Bundlers do it for you when they can prove that code is unused. - Replace heavy libraries. A small date helper or a native browser feature may replace a large package. See removing jQuery and legacy libraries.
- Remove duplicates. The Duplicated JavaScript insight asks you to remove large, duplicate modules from bundles. A common cause is two packages that depend on different versions of the same library.
- Review your dependencies when you add them. A package may bring dozens of others with it.
Step 3: stop sending old-browser code to new browsers
To support older browsers, build tools add polyfills and rewrite newer syntax into older syntax. That makes files larger. The Legacy JavaScript insight in Lighthouse 13.5 reports scripts where such code would save 5,000 bytes or more if removed. Its advice is to change the build so that it does not transpile features that all current browsers support, unless you know that you must support older browsers.
The practical steps are:
- Find out which browsers your visitors really use, from your analytics.
- Set the build tool's target list to match. web.dev's guide uses a preset that transpiles only what the chosen browsers need.
- Rebuild and compare the file sizes.
Older guidance suggested serving two builds, one with type="module" for modern browsers and a fallback for the rest. A single modern build is simpler if the browsers you must support are recent.
Step 4: split the bundle
Code splitting means sending several files instead of one and loading each when needed. MDN names two kinds: splitting by entry point, which separates the code for each page or route, and dynamic splitting, which separates code that is loaded with a dynamic import().
A dynamic import is a function-like expression that loads a module on demand and returns a promise. web.dev's example loads a library only after a form is submitted:
form.addEventListener('submit', async (e) => {
e.preventDefault();
const { default: moduleA } = await import('library');
moduleA.doTheWork();
});The code of the module is no longer part of the first bundle. It is fetched when it is first needed.
What to split off:
- Routes and pages. Each page loads its own code. Many frameworks that use webpack provide a simple way to split by route or by component.
- Features few visitors use, such as a chart, a rich-text editor, a map, a video player or a checkout step.
- Dialogs and panels that open on a click.
- Administration code that ordinary visitors never run.
Two cautions. First, a chunk that is only discovered after another script has run adds a step to the chain of requests. For code the page will certainly need, add a modulepreload or preload hint so it is fetched early. See resource hints. Second, do not split into hundreds of tiny files. With HTTP/2 and HTTP/3 several files are not a problem, as HTTP/2 and HTTP/3 explains, but each file still has a cost.
web.dev also advises keeping third-party packages in a separate vendor chunk. It changes less often than your own code, so visitors can keep it in their cache after you release an update.
Step 5: cache and compress
Give the files names that change with their content, serve them with a one-year lifetime and immutable, and compress them. See HTTP caching headers and text compression. A visitor who has already downloaded a chunk does not download it again until it changes.
Step 6: keep it from growing back
- Set a budget for JavaScript, with a warning level and an error level. MDN describes how a bundler or a build step can fail a build that crosses it. See page weight and requests.
- Check the bundle after every large dependency change.
- Review third-party tags regularly. See third-party scripts.
If you cannot change the build
On a hosted builder or a theme-based CMS you rarely control bundling. The same ideas apply through different levers: remove plugins and apps that load scripts on every page, load page-specific scripts only on their pages, and delay what the first screen does not need. See defer, async and module scripts.
Check the result
- Compare the size of the JavaScript requests before and after, in compressed and unpacked terms.
- Run Lighthouse and check Reduce unused JavaScript, JavaScript execution time and Total Blocking Time.
- Open the Coverage tool again and compare the unused share.
- Watch Interaction to Next Paint in the field. See the INP guide.
Check your own page
Run the free audit and look at the largest JavaScript files, Reduce unused JavaScript, and the Legacy JavaScript and Duplicated JavaScript insights. A large file that is mostly unused on the home page is the best place to start.
Sources
- Reduce JavaScript payloads with code splitting and Remove unused code on web.dev.
- Serve modern code to modern browsers on web.dev.
- import(), tree shaking and Lazy loading on MDN.
- Page Weight chapter, Web Almanac 2025: JavaScript bytes and unused code.
Start here
Free website audit
Live Google data for any public page: mobile and desktop.
Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it
Rather have this done for you? Speed optimization · Book a call