Looking to add new functionality to your app? Don’t want to re-invent the wheel? It’s easy to jump to a third-party library in order to save time, but there’s always important hidden costs that are often forgotten about.
In my previous article, Why I no longer use D3.js, there were comments that raised concerns about why I wouldn’t use an existing library. Below I will outline some points I personally consider before deciding whether or not to use a third-party library.
Time Spent Searching
Finding the right library that fulfills a requirement can take a bit of time. You might already be familiar with some libraries that you regularly use, and that might be a good time-saver. But what if you’re not? Which takes longer, finding the right library to import, or writing the code yourself? This might be an easy decision for complex libraries such as maps or virtual DOMs, but if it’s smaller, you can probably reduce development time by just writing the code yourself.
You’re using a library, and you’ve run into a critical bug. What are your choices? Do you write an issue on the maintainer’s issue tracker? What if the maintainer is slow to respond? What if the maintainer has a difference of opinion? Will you switch to a different library? Will you fork the library and maintain your own version? Do you try to work around the issue? Do you tell your clients that you cannot fix the issue because of a third-party?
For businesses, this can be a significant factor. Many businesses would opt in to paid support, just so that they can get their issues prioritised and fixed as soon as possible. Remember that most open-source software is done by developers in their spare time. They have no obligation to help you if something goes wrong.
When you import a dependency into your project, you’re giving a stranger the privilege of being installed and bundled into your project. This privilege is not to be underestimated. While thankfully these issues are spotted relatively quickly, for a business, even a single instance of this occurring can potentially cause irreparable harm and cost a serious amount of money.
When you’re bundling your code, you should always check exactly what is going inside your bundle. Any security flaws that end up in production, they went through you. Are you willing to be responsible for every line of code that you are shipping inside your bundle?
Is the author following semver? NPM works on the assumption that every single library author out there is correctly using semver. Many authors however, don’t follow semver, or have a difference of opinion on how semver should be applied. Does the author have a history of breaking the API? If there’s a bug, do you have to migrate your code? Is there hot fixes for older major versions?
While we like to think that our projects will follow best practices and that we will refactor code as necessary, what typically ends up happening, is that the code ships, priorities change, and refactoring never occurs. In my experience, businesses don’t like changing what’s not broken, and will prefer minimal changes over large risky refactors. Backwards compatibility support as a result is important to consider for long-term projects.
Many businesses with legal teams tend to require developers to list every single dependency they are using in their projects, and their respective licenses. You should make sure you understand what licenses you are using, what the different licenses require of you, and what restrictions your company puts on you when it comes to using open-source software.
If you’re using a component library, you should check to see how much you can customise the components. As the saying usually goes, good design is transparent, meaning the smallest of details can significantly impact your business’s brand. Visual design, motion design, performance, interactions, accessibility, all of these things affect the business brand.
Does the component library provide minimal styling that you can easily extend, or is it very opinionated in its styling and you have to use !important everywhere? Are the CSS classes backwards compatible? Is the component accessible? Are there keyboard controls and are the keyboard controls consistent with your other components? Are mouse interactions consistent with other components? If there’s animations, does the component conform to your brand’s motion design guidelines and can you change them?
Does the library export as an ES Module?
There is no reason a library should not be exporting as ESM. It is now the standard module format. Browsers support it, NodeJS supports it, and all bundlers support it. As for why you would want to use ESM, ESM helps to reduce your bundle size by not requiring to wrap the module in a CommonJS function wrapper, and allows you to avail of tree-shaking optimisations so you don’t have to import the entire library into your final bundle.
Do you really need the edge case support?
Library authors often try to cater to as wide of an audience as possible. This means that additional code is written to address very specific use cases. This can be useful from time to time, but it does come at a cost of increasing the size of your bundles.
Will a custom solution that you write be able to address all of the edge cases that the library author discovered? Maybe, maybe not. But why be concerned about that? Do you want to increase the size of your bundle in order to handle edge cases that are likely not applicable for your use case?
If you do want to use a library for something, look for libraries that specifically aim to provide a lot of functionality in the smallest file size possible. For example, instead of React and only using a subset of its features, consider using Preact which provides the common functionality with a smaller file size. Instead of OpenLayers to render simple maps with simple feature vectors, consider using Leaflet instead.
Is the library well-tested? A good suite of test cases indicates what exactly the author has checked for and what could potentially be an issue for you. Also in future releases of the library, those test cases help to act as a way to verify backwards compatibility.
Remember however, that a library having test cases does not mean you don’t have to write test cases yourself. A library is simply an implementation detail. You should still be testing your functions to make sure that the input you provide gives the expected output. Your project is still your responsibility, it’s not the library author’s responsibility. Make sure you’re confirming that functionality is working the way you expect it to.
Do you really need the module?
Some people say, “you shouldn’t reinvent the wheel no matter how small the wheel”. All you have to do is look through your node_modules directory, and you’ll see a huge amount of ridiculously small libraries.
One of my favourite examples that demonstrates what I’m talking about is home-or-tmp. You probably have this sitting in your node_modules, it’s a fairly common library. The version most people have installed has 2 dependencies, and one line of code. I would argue that for small libraries like this, it’s never worth using, and the risks of using such libraries are far larger than the benefits. Take the incident with left-pad, a small library that shouldn’t have been difficult for any developer to write, but when removed, caused builds to break unexpectedly worldwide.
Even if you do want to use a small code snippet, rather than using it as a dependency, consider inlining it into your code instead. That way, you avoid all of the administrative risks associated with using a dependency, and you can tweak the code as necessary. I wouldn’t do this for anything larger than a few lines of code however, as generally the benefits of having an external dependency start to significantly outweigh the benefits of inlining.
Am I saying you shouldn’t use libraries? No, of course not, use libraries where they make sense to use. Reusing existing functionality is a huge productivity boost. What I’m trying to highlight here, is that you should never forget the costs associated with using a library. The argument of “don’t re-invent the wheel”, while it sounds logical, trivialises the costs and puts developers and their business into a vulnerable position. We should be more conscious about the access we’re giving to third parties, about the responsibilities that we are delegating to third parties, about the code we are distributing to our users, and much more. At the end of the day, if anything goes wrong, it’s not the library author’s responsibility, it’s our responsibility.
Thanks for reading!