DEV Community

Adam Golan
Adam Golan

Posted on

The Hidden Cost of Third-Party Libraries: When 'Don't Reinvent the Wheel' Goes Wrong

"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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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);
  };
};
Enter fullscreen mode Exit fullscreen mode
  1. Core Business Logic
    Your core business logic should usually be custom-built. It's what differentiates your product and requires precise control.

  2. 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:

  1. Complex Algorithms

    • Cryptography
    • Data compression
    • Complex mathematical operations
  2. Battle-Tested Solutions

    • Authentication systems
    • Database ORMs
    • Testing frameworks
  3. 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);
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Audit Before Adding

    • Check bundle size impact
    • Review security vulnerabilities
    • Assess maintenance status
  2. Consider the Full Cost

    • Learning curve
    • Integration time
    • Long-term maintenance
    • Team familiarity
  3. 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)