/ โ† JavaScript Checker Fixes / JS Bundle Size
In this guide
  1. Why bundle size costs more than download time
  2. Finding out what is in the bundle
  3. The usual reasons a bundle is too big
  4. Splitting code with dynamic imports
  5. Tree shaking: letting the build remove dead code
  6. Build targets and polyfills
  7. The fix, step by step
  8. Edge cases and trade-offs
  9. Keeping the bundle small
  10. Questions and answers

Why bundle size costs more than download time

It is tempting to think of a large script as a bandwidth problem that compression and a fast CDN will solve. Compression does shrink the transfer, often to a quarter or less of the original, but the browser decompresses it again before doing anything with it. What the phone has to work through is the full, uncompressed code.

That work happens in stages, and most of it is on the main thread, the same thread that responds to taps and draws the page:

  1. Download. Affected by transfer size, the number of requests and whether the file is cached.
  2. Parse and compile. The JavaScript engine reads the text and turns it into something it can run. The cost grows with the amount of code, whether or not that code is ever called.
  3. Execute. Top-level code in every module runs straight away: registering components, building lookup tables, setting up listeners. A large bundle usually means a lot of this start-up work.

So a 400 KB bundle that compresses to 110 KB still means 400 KB of parsing and whatever start-up work those modules do. On a developer laptop this passes unnoticed. On the kind of phone many customers actually use, it shows up as long tasks during load and a sluggish first tap, which is why heavy bundles tend to appear alongside poor Interaction to Next Paint.

๐Ÿ’ก When comparing sizes, be clear which figure you mean. Network panels usually show both the transferred (compressed) size and the resource (uncompressed) size. Track the uncompressed figure for parse cost and the compressed one for download time.

Finding out what is in the bundle

You cannot trim what you cannot see. Three views together give a full picture.

1. The checker and the Network panel: what each page loads

The JavaScript Checker lists every script the page loads with its size and the time spent parsing and running it. Run it on each main template: home, category or listing, product or article, basket, and any logged-in dashboard. Note which files are shared by every template and which are specific to one. In Chrome DevTools, the Network panel filtered to JS shows the same files with transferred and resource sizes side by side.

2. A bundle analyser: what each file is made of

An analyser reads your build output and draws a treemap, one rectangle per module, sized by bytes. The biggest rectangles are where the savings are.

Build setupHow to see the treemap
Any tool that writes source mapsnpx source-map-explorer dist/assets/*.js
Vite or RollupAdd rollup-plugin-visualizer to the plugins list and build; it writes an HTML report.
webpackwebpack-bundle-analyzer as a plugin, or run it against a stats.json file.
esbuildBuild with the metafile option and load the file into esbuild's online analyser.

3. The Coverage panel: what actually runs

Open DevTools, press Ctrl+Shift+P (Cmd+Shift+P on a Mac), type "Coverage" and start recording, then reload the page. Each script gets a bar showing how many bytes were used during the session. A file that is 70 per cent unused on the home page is a strong candidate for splitting. Coverage only counts what ran during your test, so click through the menu, search and basket before trusting the number.

โš ๏ธ Analyse the production build, not the development server. Development builds include extra checks and are not minified, so their sizes are meaningless for this job.

The usual reasons a bundle is too big

What you see in the treemapWhy it happensWhat to do
One enormous file shared by every pageEverything is imported from a single entry point.Split by route so each page loads its own code.
A whole utility library when you use three functionsCommonJS builds or default imports defeat tree shaking.Use the ES module build and named imports, or native equivalents.
Every locale of a date or charting libraryThe library bundles all locales by default.Import only your locale, or use the built-in Intl APIs.
Two copies of the same packageDifferent dependencies ask for incompatible versions.Check with npm ls <package>, align versions, then run npm dedupe.
Large polyfill blocksThe build targets browsers you no longer need to support.Update your browser targets.
Rich text editors, maps or charts on pages that rarely use themImported at the top of a shared file.Load them with a dynamic import when needed.
JSON data, icons or SVGs inlined as stringsImported into code rather than fetched.Serve them as separate cacheable files.

Splitting code with dynamic imports

A static import at the top of a file puts that module into the same bundle. A dynamic import() tells the build tool to create a separate chunk that is fetched only when the line runs. That one change is the main lever for cutting what each page loads.

// Before: the chart library is in the main bundle for every page
import { renderChart } from './charts.js';

// After: fetched only when the reports tab is opened
document.querySelector('#reports-tab').addEventListener('click', async () => {
  const { renderChart } = await import('./charts.js');
  renderChart(document.querySelector('#chart'));
});

Good candidates for their own chunk are routes most visitors never reach, dialogs and drawers, editors, maps, charts, video players, and anything behind a login. Poor candidates are the code for the first screen, which you want as early as possible, and tiny modules, where a separate request costs more than it saves.

Two refinements keep split code feeling instant:

Tree shaking: letting the build remove dead code

Tree shaking is the build tool dropping exports nobody imports. It only works when the code is written as ES modules and the tool can be confident that removing a module has no side effects. Three things commonly stop it:

  1. CommonJS packages. Code using require and module.exports cannot be analysed reliably, so the whole package is kept. Many libraries publish an ES module version, sometimes under a different name ending in -es.
  2. Importing everything. import * as utils or a default import of a large object pulls in the lot. Import the named functions you use.
  3. Missing side-effect information. If your own package or a library lacks a sideEffects field in package.json, the bundler has to assume importing a file might change something globally and keeps it.
{
  "name": "example-shop-ui",
  "type": "module",
  "sideEffects": ["*.css", "./src/polyfills.js"]
}

Listing only the files that genuinely do something on import (stylesheets, polyfills) lets the bundler drop every other unused module. Get this wrong and a stylesheet or a registration file silently disappears from the build, so test after changing it.

Build targets and polyfills

Build tools transform modern syntax and add polyfills for the browsers you tell them to support. A target list written years ago can force the build to rewrite classes, async functions and optional chaining into longer older code, and to include polyfills every current browser already has.

Check the browserslist entry in package.json, the build.target setting in Vite, or the target in your Babel or TypeScript configuration. Compare it with your analytics: if almost nobody visits in a browser that lacks ES module support, there is little reason to transform code for it. Also look for polyfill libraries imported wholesale at the entry point and replace them with targeted polyfills or none at all.

โš ๏ธ Check customer data before dropping older browsers. Some audiences, such as public-sector staff on managed machines or visitors on older tablets, lag behind the average. If you still need them, serve a separate legacy build rather than making everyone download the old code.

The fix, step by step

Step 1

Record a baseline for each template

Run the JavaScript Checker on your home, listing, product or article, basket and account templates and write down, for each, the total script size and the parse and execution time it reports. Save the list of files per template. Also note which scripts appear on every page. Without these starting figures you cannot tell which change helped, and it is easy to shrink one file while another quietly grows.

Step 2

Open the bundle and name the big pieces

Build for production with source maps and open the output in source-map-explorer or rollup-plugin-visualizer. List the ten largest modules and, next to each, the pages that really use it. Then record a Coverage session in DevTools on the home page while using the menu and search. Modules that are large, shared and mostly unused on common pages go to the top of your list.

Step 3

Split the code by route and by feature

Give each route its own entry or lazy chunk so a visitor on an article page never downloads the checkout code. Then move heavy features that sit behind a click, such as charts, maps, editors and dialogs, behind a dynamic import(). Start fetching those chunks when the visitor hovers or scrolls near the trigger, and keep everything needed for the first screen in the main chunk so it is not delayed.

Step 4

Make tree shaking actually work

Replace namespace and default imports of large libraries with named imports, and switch CommonJS packages to their ES module builds where one exists. Add a sideEffects field to your own package listing only stylesheets and genuine set-up files. Rebuild and compare the treemap with the one from step 2. If a library still appears whole, check its documentation for a per-function import path.

Step 5

Replace or drop heavy dependencies

For each large package, ask whether the browser can now do the job itself: Intl.DateTimeFormat and Intl.NumberFormat cover most date and currency formatting, fetch replaces request libraries, and modern DOM methods replace many helper libraries. Where you still need a library, look for a smaller one with the same features. Use npm ls to find duplicate versions and align them so only one copy is bundled.

Step 6

Stop shipping code for old browsers

Check the browser targets in browserslist, Vite's build.target or your Babel and TypeScript settings against the browsers your visitors actually use. Raise the targets so modern syntax is left alone and unnecessary polyfills are dropped. Remove blanket polyfill imports from the entry file. If a real share of customers still needs older browsers, build a separate legacy bundle for them instead of penalising everyone else.

Step 7

Set a budget and enforce it in the build

Pick a size limit for the main chunk and for each route based on your new, smaller figures, and add a check such as size-limit or your bundler's size warnings to continuous integration so a pull request that breaks the budget fails. Re-run the JavaScript Checker on every template to confirm the saving, then compare real-user INP over the following weeks to see whether responsiveness improved.

Edge cases and trade-offs

Keeping the bundle small

Questions and answers

1. What is a good JavaScript bundle size?

There is no official limit, and Google does not publish one. The useful question is how much script a page must download, parse and run before it responds, measured on the kind of phone your customers use. Start from your own baseline, cut what each page does not need, and set a per-route budget slightly above the result. A content page that needs no interactivity should carry very little, while a web app dashboard can justify more.

2. Does gzip or Brotli compression solve a large bundle?

It solves part of it. Compression reduces the bytes sent over the network, which shortens download time, and you should always have it switched on for JavaScript. But the browser decompresses the file before parsing it, so the engine still has to read and compile every line, and the top-level code in every module still runs. That processing is usually the bigger cost on phones, and compression does nothing to reduce it.

3. How do I find unused JavaScript on a page?

Open Chrome DevTools, open the command menu with Ctrl+Shift+P or Cmd+Shift+P, choose Show Coverage, start recording and reload the page. Each script shows how many of its bytes ran. Use the page as a visitor would, opening the menu and search, before reading the figures, because Coverage only counts code that ran during your session. Code that is unused on most pages but needed on a few is a candidate for splitting rather than deletion.

4. What is the difference between code splitting and tree shaking?

Tree shaking removes code that nothing imports, so it never reaches any bundle. Code splitting keeps code that is needed somewhere but moves it into separate files loaded only on the pages or at the moments that need it. You want both: tree shaking cuts the total amount of code in the build, and splitting controls how much of what remains each individual page has to download and run before it becomes usable.

5. Why did my bundle get bigger after I added one small package?

Because the package brought its own dependencies, or a copy of a library you already had in a different version. Run npm ls with the package name to see its dependency tree, and look at the bundle treemap before and after. Packages published only as CommonJS can also defeat tree shaking, so the whole library is included even if you call one function. Check for an ES module build or a lighter alternative.

6. Will lazy loading JavaScript hurt SEO?

Not if you lazy load the right things. Search engines need the page content, links and metadata, which should be in the server HTML or rendered on load, not behind a click. Charts, editors, dialogs and account features that a crawler would never use anyway are safe to load on demand. What causes problems is lazy loading the main content itself or links that only appear after an interaction, because crawlers do not click.

7. Should I remove all polyfills?

Remove the ones your visitors do not need. Most polyfills in older projects cover features every current browser supports, and they add weight for everyone. Check your analytics for the browsers people actually use, then raise your build targets and remove blanket polyfill imports. If a meaningful group still uses an older browser, serve them a separate legacy build so modern browsers are not made to download code written for a handful of old ones.

See what your scripts cost

The JavaScript Checker loads your page in headless Chromium, lists every script with its bundle size and parse and execution time, captures console errors and warnings, and shows which third-party tags add to the weight.

Open JavaScript Checker Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All JavaScript Checker Fixes  ·  Fixing Long Tasks  ·  Fixing Render-Blocking Scripts  ·  JS Checker Guide  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.