"Don't reinvent the wheel" is one of programming's most sacred principles. But like many absolute statements in software development, the reality is more nuanced. Let's explore when importing that shiny npm package might cost more than rolling your own solution.
The Illusion of Free Code
We've all been there: you need to implement a feature, and there's a package that promises to do exactly what you want. It's popular, well-maintained, and just an npm install
away. What could go wrong?
// Just a simple date formatter, right?
npm install moment
// Later in your code...
import moment from 'moment' // + 231KB to your bundle
const formattedDate = moment(date).format('MMMM Do YYYY');
// VS writing it yourself
const formatDate = (date) => new Date(date).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
}); // ~300 bytes
That innocent import just added 231KB to your bundle size. For comparison, the entire jQuery library is about 87KB.
The Real Costs
1. Bundle Size Bloat
Let's look at a real-world example of how dependencies can spiral:
// You need a simple deep clone
npm install lodash // 531KB
// VS native solution
const clone = obj => structuredClone(obj);
// You want to validate emails
npm install validator // 364KB
// VS simple regex
const isValidEmail = email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
Each library adds to your bundle size, increasing load times and potentially hurting your user experience.
2. Security Vulnerabilities
Remember left-pad? The infamous 11-line package that broke half the internet? Here's what it did:
// The entire left-pad package
function leftPad(str, len, ch = ' ') {
str = String(str);
let i = -1;
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}
// VS native solution
const leftPad = (str, len, ch = ' ') => str.padStart(len, ch);
Every dependency is a potential security vulnerability. The more dependencies you have, the larger your attack surface becomes.
3. Version Lock-In
Here's a scenario many developers have faced:
// Your package.json
{
"dependencies": {
"major-framework": "^4.0.0",
"useful-utility": "^2.0.0"
}
}
// Error: useful-utility@2.0.0 requires major-framework@^3.0.0
Congratulations, you're now stuck between a rock and a hard place, unable to upgrade one package without breaking another.
When to Build Your Own
Here's a decision framework I use:
- Simple Functionality
// Don't import a library for this
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) },
(_, i) => arr.slice(i * size, i * size + size)
);
// Or this
const debounce = (fn, ms) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), ms);
};
};
Core Business Logic
Your core business logic should usually be custom-built. It's what differentiates your product and requires precise control.Performance-Critical Code
When performance is crucial, custom solutions tailored to your specific use case often outperform generic libraries.
When to Use Third-Party Libraries
There are absolutely times when third-party libraries make sense:
-
Complex Algorithms
- Cryptography
- Data compression
- Complex mathematical operations
-
Battle-Tested Solutions
- Authentication systems
- Database ORMs
- Testing frameworks
-
Standard Solutions
- UI component libraries
- State management tools
- HTTP clients
The Middle Ground: Micro-Dependencies
Sometimes, you can find a happy medium:
// Instead of importing the entire lodash library
import get from 'lodash/get' // Only what you need
// Or better yet, use native solutions when possible
const get = (obj, path) =>
path.split('.').reduce((acc, part) => acc?.[part], obj);
Best Practices
-
Audit Before Adding
- Check bundle size impact
- Review security vulnerabilities
- Assess maintenance status
-
Consider the Full Cost
- Learning curve
- Integration time
- Long-term maintenance
- Team familiarity
-
Plan for Removal
- Wrap third-party code in your own interfaces
- Keep implementation details isolated
Conclusion
"Don't reinvent the wheel" is good advice, but it's not absolute. Sometimes, a custom-built wheel that perfectly fits your vehicle is better than a generic one that mostly fits. The key is understanding the trade-offs and making informed decisions.
Remember: every line of code is a liability, whether you wrote it or imported it. Choose your liabilities wisely.
Have you ever regretted adding a dependency? Or found a case where building your own solution saved the day? Share your experiences in the comments!
Top comments (0)