Stop Importing Entire Libraries
I recently audited a project that imported all of Lodash to use three functions. The bundle included 24kb of compressed JavaScript the application never executed. Multiply this pattern across five libraries and you’re shipping 100kb of dead code to users. It’s wasteful and avoidable.
The Default Import Problem
When you write import _ from 'lodash', you’re importing the entire library. Even if you only use _.debounce, the bundler includes everything unless tree-shaking removes it. Tree-shaking works sometimes but not reliably across all library types and bundler configurations.
The safer approach is named imports: import { debounce } from 'lodash'. Modern bundlers can tree-shake this more effectively. But even better is importing directly from submodules: import debounce from 'lodash/debounce'. This guarantees you only include what you need.
Some libraries structure their exports to make tree-shaking easy. Others don’t. Checking how a library organizes its exports before adoption saves frustration later. Libraries with barrel files that re-export everything can defeat tree-shaking efforts.
Real Bundle Impact
I measured this on a production application. Switching from default imports to named imports reduced the vendor bundle by 87kb compressed. That’s 240kb uncompressed—enough to noticeably affect load time on slower connections.
The biggest wins came from date libraries like Moment.js and utility libraries like Lodash. Moment includes every locale by default. Most applications need one or two locales maximum. Using a more modern alternative like date-fns with explicit function imports cut date handling code by 60kb.
UI component libraries also bloat quickly. Importing the entire Material-UI library when you use five components wastes bandwidth. Most mature component libraries support tree-shaking, but you need to configure it correctly. Reading the docs saves megabytes.
How to Audit Your Bundle
Webpack Bundle Analyzer visualizes what’s in your production bundle. Install it, run a build, and examine the output. Large chunks of unused code become immediately obvious. You’ll spot libraries you forgot about and modules imported accidentally.
Look for duplicates too. Sometimes you have multiple versions of the same library because dependencies use different versions. Bundle analyzers highlight this. Resolving to a single version through package.json resolutions field can eliminate redundancy.
Check the gzipped sizes, not just raw JavaScript. Some libraries compress well while others don’t. A 50kb library might only add 10kb gzipped while another 50kb library adds 30kb. Compression characteristics matter for real-world performance.
Alternative Strategies
For utility functions, consider writing your own implementations. Lodash’s debounce is about 30 lines of code. If you understand what it does, implementing it yourself removes a dependency entirely. This only makes sense for simple utilities, not complex algorithms.
Some libraries offer lightweight alternatives. Instead of Moment.js, use day.js or date-fns. Instead of full Lodash, use lodash-es which is built for ES modules and tree-shakes better. Instead of jQuery, use vanilla JavaScript for simple DOM manipulation.
Micro-libraries that do one thing well often beat importing one function from a large library. A dedicated debounce package might be smaller than Lodash even if you import just one function, because the package has less overhead.
Development Workflow
Set up bundle size monitoring in your CI pipeline. Tools like bundlesize let you define maximum sizes for each bundle. If a PR increases bundle size beyond thresholds, the build fails. This catches bloat before it reaches production.
Code reviews should check imports. When someone adds a library, ask if we need the whole thing. Could we use a lighter alternative? Is there a browser API that solves this now? These questions prevent accumulation of unused code.
Organizations like an AI consultancy that I’ve worked with have started incorporating bundle size as a performance metric in their development processes. Treating bundle size like a bug—something to fix rather than tolerate—changes team behavior.
The API Shift
Some modern libraries offer compositional APIs that encourage importing only what you need. React hooks are an example. Instead of one large Component class with all lifecycle methods, you import specific hooks. This naturally limits what enters your bundle.
GraphQL clients often support code splitting better than REST libraries. You define exactly what data you need, and bundlers can optimize around that. This architectural decision affects bundle size at a fundamental level.
Static site generators and frameworks like Next.js make bundle optimization easier through automatic code splitting. But you still need to be careful about what you import in each module. The framework can’t fix wasteful library imports.
Edge Cases and Tradeoffs
Sometimes including more of a library makes sense. If you use ten Lodash functions, importing each individually becomes unwieldy. At some threshold, importing a larger chunk of the library provides better developer experience with acceptable bundle cost.
Dynamic imports let you load expensive libraries only when needed. If you have a rarely-used admin feature that needs a complex library, dynamic import loads it on demand. Most users never download that code. This pattern works well for modals, dialogs, and admin panels.
Server components in newer frameworks change the calculation. If code runs only on the server, bundle size doesn’t matter to the client. Knowing what executes where lets you make different import decisions in different parts of your application.
Measuring Real Impact
Bundle size matters, but measure whether reducing it actually improves user experience. Run Lighthouse audits before and after optimization. Check real-world performance metrics like First Contentful Paint and Time to Interactive.
Sometimes a 50kb bundle reduction doesn’t change user experience measurably because other bottlenecks dominate. Network latency, server response time, or image optimization might provide bigger wins. Don’t optimize bundle size at the expense of more impactful improvements.
That said, bundle size is easy to measure and fix. The tooling is good. So even if it’s not the biggest performance problem, it’s often worth addressing because the effort is bounded and the tools are reliable.
Making It Habitual
The best time to think about bundle size is when adding dependencies. Before installing a package, check its size on bundlephobia.com. If it’s large, look for alternatives or consider implementing the functionality yourself.
Review dependencies quarterly. Remove packages you no longer use. Update to newer versions that might be smaller or tree-shake better. Dependencies accumulate like clutter—periodic cleaning keeps the codebase maintainable.
Make bundle size visible. Display it in your README or documentation. When the team sees “Current bundle: 234kb gzipped” at the top of the project, it becomes a metric to care about. What gets measured gets managed.
Practical Rules
Import from submodules when possible: library/function instead of library. Use named imports over default imports for better tree-shaking. Check bundle analyzer output quarterly. Set up automated bundle size monitoring. Consider alternatives before adding large dependencies.
These simple practices prevent bundle bloat without complex build configurations or constant optimization. They become habits that compound over time, keeping applications fast as they grow.
Shipping less JavaScript makes applications faster, more accessible, and cheaper to host. It’s not about premature optimization—it’s about making conscious decisions at each dependency addition. The cumulative effect of many small choices determines whether you ship 100kb or 500kb to users.