Published on

Web Dev Woes: Juggling Dependencies without Bloating Bundles or Breaking It All

Authors
  • avatar
    Name
    Callum Gander
    Twitter

Intro

Over the past year, I've experimented with a lot of different web dev libraries and frameworks: Svelte, React, Next, React Query, to name but a few and I wanted to share some of my knowledge with others in the hopes it can help them with one thing in particularly: choosing libraries.

Some libraries are frameworks or meta-frameworks, like React and Next, that help abstract away difficult tasks in web development like handling rendering and manipulating the DOM. Some are utilities that focus and provide specific functionality, like Headless UI. Others are more comprehensive component libraries that provide you with base-styled components that rapidly accelerate frontend development, a la Mantine, MUI or Flowbite. Some are for the backend, others are for the frontend, and more are for both.

The aim of this article is to give readers a set of tools that will help them better evaluate libraries they're thinking about using and go over some of the tradeoffs you have to think about and make when using different libraries. I'll focus quite heavily on the frontend side of this because broadly speaking, I've found the most issues in terms of tradeoffs with frontend libraries, but will also cover some backend ones too. This is also going to be tailored pretty heavily towards less experienced developers, but who knows? Maybe more experienced people might be able to get a thing or two out of it too.

Frontend vs. Backend

The reason for worrying more about frontend libraries is pretty simple; you control way less about the client than you do the server. The client can be on a range of devices, all being slightly different sizes, all with different processing and memory capabilities, all running in a variety of browsers, on different operating systems, with users with varying degrees of understanding and knowledge. Add to this the fact that given all this, it's arguably harder to test your frontend than it is your backend, even with modern tools like Puppeteer and Cypress.

Contrast this with the backend where you are usually running your code on a single operating system, with known capabilities, with users being heavily restricted in the ways they can interact with the API and usually just interacting with it indirectly. These also reduce a lot, but not all, of your worries about bundle size. This isn't to attack backend. Obviously, managing a Kubernetes cluster has its own set of complications and worries, but given that most of the problems I found were with frontend libraries, I'm going to mainly focus on them.

Why you should be evaluating libraries

So, why does this even matter? Well, libraries generally make development a lot easier by abstracting away the complexity of complicated tasks into simpler ones. Most of the software we use and interact with today was possible due to libraries that made complex things simpler to build. Let's take a simple example here: React. React makes building UI systems simpler (at least in theory) by giving you patterns and tools for managing state and breaking up code into reusable, modular components. State goes from parent to child (again, at least in theory) and we break up our code into distinct components that can be reused throughout our app. Worrying less about the complexities of specific tasks like how to set and update state opens up more time to focus on the actual problem we're solving, like building a map app or a booking app. Libraries are crucial for doing this in modern web dev.

A quick distinction here that should be made is between libraries and frameworks, both of which I'll touch on. Libraries are collections of reusable code for doing tasks, and may or may not give you patterns for how to use them. Frameworks are a bit broader than that and usually have specific patterns and conventions for doing things. They overlap a lot, and I'm going to be using them pretty interchangeably, but there is a difference here.

At least in theory

Libraries should save you development time, but, as we shall see, don't always. Bad tools can wreck your productivity and cause a whole host of additional issues, from compatibility issues that ruin your users' experience to bloating your bundle and costing you hundreds more in egress bandwidth each month. Integrating the wrong libraries into your app can make your life a living hell.

The inverse of this is also true. A good library can massively augment your ability to do certain tasks and allow you to build orders of magnitude faster than before. A quick example of this is that I used a certain backend framework for a while, cough Strapi cough, that let's say inhibited my productivity a lot. After switching to the T3 stack, my productivity skyrocketed, and I stopped dreading development each day. A poorly built tool makes problems for you, a well-built one reduces them.

To help you avoid this fate, we'll look at issues, such as styling, the cognitive overhead they add, how they add to the bundle size, the hidden costs of using a specific library, and more. Hopefully, this will give you a decent toolkit for deciding whether a given library is worth using or not

Utility vs. Cost

Generally, you can think of picking a library as having whether some deals the required amount of utility with minimum cost. If it has a high enough utility and the cost is okay, then use it, otherwise, don't or start thinking about the consequences. After some bad experiences that we'll touch on as we go through, this has served me pretty well, and learning from each of those experiences has allowed me to pick better libraries each time and massively improve what I can build and how efficiently I can build it. Throughout this article, we've got to think about this tradeoff between utility and the cost. There are many dimensions to this cost, let's start with a simple one: styling.

Styling

Obviously, this is only an issue for frontend libraries, but it's still an important one. Depending on which CSS framework you use, a given library can easily integrate with what you're using or cause you real headaches. I cannot stress this enough: DO NOT pick libraries that use different CSS frameworks than you do unless you really need this library, can't build it from scratch, or have a large enough team that the extra cost to integrate it with whatever you're already using/reducing the bundle cost of adding it adds. At best, it'll just bloat the amount of CSS you ship and force you to write styles in two different ways, at worst, it'll create a nonsensical mix of CSS that might collide in unexpected ways and will be a nightmare to build with and maintain.

Let me give you a concrete example here. I was working on a project which used both Mantine and Tailwind. Mantine has a really nice notification system that works exactly as I needed it to (yes, I know I should have just used react-hot-toast) and we were using a few more of their components initially. Not the worst thing in the world, right? After some configuring, they worked relatively okay together.

However, after doing most of the styling and when I was doing some performance work, I realised that Mantine was bloating the bundle too much for the limited functionality we were using it for, even with tree shaking. So, I decided that I'd remove it. The issue was that I'd already finished doing most of the styling, but Mantine's ThemeProvider that's needed to provide the default Mantine styles and add themes etc. had been injecting styles that the current styling had been built on top of. When you removed the MantineProvider, almost every single page at every breakpoint substantially changed.

It was not a quick fix and there was a deadline, so I had to just leave it there and accept the overhead Mantine added to the bundle, losing performance gains. Was this the worst thing to ever happen? No, but it still damaged the readability of the code and added unnecessary packages to the bundle. This is just one way using more than one component library with a different styling framework can impact you.

It's not just mistakes like this that can occur, but more often just wasted time trying to figure out how to approach something in a different style of CSS than you're used to. Some libraries don't expose any classes, so you have to manually go through the library's CSS or the HTML with the inspector in Chrome to find the class names and then overwrite them. It's a pain, and it's rarely worth the time spent on it. Generally, there's a library out there for your specific CSS framework, find it and use that. If not, ask if it's that hard to build, if it's not, do so. Otherwise, just be cognisant of the tradeoff you're making and allow time for it.

Lack of utility or overly complex

Another issue I realised after playing around with some Tailwind component libraries was that some just completely lacked enough to be worth using beyond a component or two, which you could just code yourself pretty rapidly and save yourself another dependency. The libraries would require you to install a bloated NPM package despite the complete lack of actual functionality.

Others, specifically some individual component libraries, like Sliders, felt like they forced you into doing things a specific way that doesn't gel with your current approach or just added an unnecessary amount of work for the limited functionality you need. This isn't a critique of those libraries, in and of themselves. Many users of the library may need that functionality, but often you don't, and the added bloat and dependencies of that dependency that are needed to add such functionality could genuinely damage your performance or just add an unnecessary headache.

This issue happened a lot when I first started playing around with component libraries. It was easy for me to just go into a "oooh shiny shiny" type of mindset of just seeing that a library had a thing or things I needed and instantly incorporating them into a project without properly tinkering with them and figuring out if it actually made my life easier and reduced the amount of work I needed to do or just added time looking through docs trying to figure out how to do the one thing I needed that library to do and added additional code.

Nothing like this stuck out more to me than Redux. For those who don't know, Redux is a library for managing application state with or without React. It was notoriously a pain to use and very boilerplate heavy. I know now we have RTK, and it's not that bad, but when I first encountered Redux, it just frustrated me. It felt overly obtuse without clear explanations for why. Why do I need a reducer? Why is it explained in these terms? Why do I need so much god-damn boilerplate? Eventually, we got libraries like Zustand and Jotai that took the basic idea of Redux, took away most of its complexity and boilerplate and made it super simple to get the same functionality. Zustand massively helped me get things done quickly, Redux repeatedly got in my way and demoralized me.

It's true that state management is incredibly complex, but when you're just building an app, and you need to defy the laws of React by moving state between different places, you don't want to go into a rabbit hole of functional programming and immutability. You just want to solve the problem. I'm not saying don't stop and learn if you'd like to, I enjoyed it, but it also massively wasted my time learning it when I could have just used Zustand and avoided most of that. Sometimes you want to learn, other times you just need to get it done.

The bundle and the mind: Increased overhead

As I touched on already, relying on many different libraries also adds overhead, both cognitively and in terms of bundle size. Cognitively, having to manage the different ways in which components function and interact with other parts of the system can be a real pain. For example, you may treat your state one way in the majority of an app, but a specific component library wants you to pass that state in a specific format, different to the way you've currently been handling everything. This would involve an additional transformation function or rewriting code elsewhere, prompting the question of whether it's worth the cost of doing so. This can cause real issues with maintainability and just general readability if one component, say, is demanding you use it in a Class based, object orientated manner and another is demanding you use it in a Function based, functional manner.

I'd say this is less of an issue with modern packages, but still definitely an issue with some older packages that were produced before JavaScript devs really started drinking the functional programming cool aid. This obviously can get worse if you're relying on several libraries and have to link them together in weird, unexpected ways. Sometimes this is necessary, other times it's just wasting time trying to get a hammer to do the job of a saw.

Bundle size is also a big concern. Some libraries are badly out of date and don't use a lot of the modern practices that help make NPM packages smaller. Moment.js is obviously a bad example of this, such that despite how core it is, most have now moved to libraries that actually fixed the bundle issue, like Luxon. Even outside of this, some just have such giant core dependencies that can't be removed with tree shaking that it may not be worth using them. There's a specific 3D package I use that was core to a specific app that shall remain unnamed. It was a great package and did what it did well, but damn was it unorthodox and out of date. It felt so hard to contribute to it because it did everything in its own unique way. One side effect of this was that there was a single dependency of this library that took up almost a 1/5th of my total bundle size. 1/5th. This wasn't necessary either, it was removable, it'd just be fiddly, but the author didn't want to do it and I couldn't spare the time to either, so nothing happened. Fortunately, there wasn't an alternative, so I knew I couldn't do anything about it, but it's worth keeping an eye out for such packages.

When we're specifically talking about component libraries, you should probably try to stick to just one high level component library, think MUI or Flowbite. As we've already seen, each subsequent component library you add, the costs, complexity and hence problems go up. Don't worry so much about including single component libraries if they're crucial to function, but just keep in mind the overhead these incur and if it's too much, and it's simple enough to implement yourself, consider removing it.

There are some things you can also do to mitigate and avoid bundle size issues as well. Firstly, you can analyse your bundle with Webpack Bundle Analyzer (https://www.npmjs.com/package/webpack-bundle-analyzer). I've always used this, and it can help you quickly spot what dependencies are bloating your bundle the most. On the editor side, if you use VS Code a plugin like https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost is great for quickly seeing how much something costs.

Just quickly as well, if you're using Next and there are packages that you only use server side and don't want to ship to the client, for a while, you were out of luck. However, you can get around this now using Zones (https://nextjs.org/docs/advanced-features/multi-zones) or Turborepo (https://turbo.build/repo/docs/handbook/what-is-a-monorepo) to split up code that's only being used by your frontend and code that's only being used by your backend, or even split your frontend into two separate ones, say your landing page and your app.

Maintenance and Growth

A further consideration is whether the libraries are being actively maintained and expanded. Avoid libraries that haven't been updated within a few months unless they provide minor functionality, and they continue to work, or, again, if there is literally no other choice. Even then, be cautious. Many just won't work due to some outdated or incompatible dependency, or require you to use a substantially older version of Node or React, which may not work in deployment.

If the library is for a complex component, yet they don't have some of the features you need, and you don't expect to be able to actively contribute yourself given your workload, you'll have to make tradeoffs whether it's worth integrating or whether you're able to contribute, both in terms of time and capability of actually being able to do the work done.

One example I had was for a while I used Strapi and, not to put them on the fire, but their migration process from v3 to v4 was a disaster. They flat out dropped core functionality like database transactions. Given the application I was working on NEEDED that as there was a lot of money on the plate per order, inconsistencies or errors could cost large amounts of money, I had the choice of either applying a hacky workaround that would require me to later radically change large parts of code or waiting. I initially decided to wait while I worked on other things, but the blunt reality was that despite it being an issue for a fair few users, it wasn't a priority for them and didn't get done for months. The “team” was basically just me and, after related issues, we decided to move away from Strapi, the transactions issue being the main reason. Sometimes you can just work on the issue yourself and contribute, but when you're short-staffed and time is very focused on the product itself, you just don't have time for this and such issues can wreck the whole development process.

If it's going to be an important library, always check the recent issues to see if they're getting resolved and replied to promptly. Otherwise, that breaking issue for you, that you just don't have the technical knowledge to solve, could at best slow you down or at worst delay your whole project. I've seen libraries whose author has seemingly died, vanished or just stopping bothering to respond to pull requests and issues, so even if you could fix the issue, there is next to no chance of getting it merged. In this case, you could always just fork it and apply the fix yourself, but it's up to you, and again, adds time you may not have.

Hidden or Obvious Costs

This is more of a component library issue, but as we'll see this isn't completely removed from broader JS frameworks either. Should you ever pay for component libraries? Maybe? It's hard to say. Firstly, regardless of what library you use, there are great free libraries out there. You may feel like you have everything that you need with them, so always check them out first. But sometimes, you want more than those offer. In particular, if you're more focused on backend and functionality in general, you may just want to cut out a lot of the boring frontend work and buy UI components from somewhere like TailwindUI or Landingfolio.

Personally, I've found this drastically cuts down the time I need to build parts of personal projects that I just fundamentally don't enjoy doing, such as almost anything to do with the landing page. There's usually not much functionality to components on these pages, it's more about just laying things out in aesthetic ways that communicate information well. Similarly, if you're a solo developer, you may just want to do this to reduce your workload.

One I'd probably advise against is small component libraries tailored towards enterprise customers that charge exorbitant prices for things that do basically the same as most free libraries and are sometimes even built off of them. An example of this could be FullCalendar. It's a good library, but is it really worth $480 for a premium licence? My opinion is pretty much no, never. The blunt reality is that they don't seem like they're ever worth it, except for those giant enterprise firms that can afford to throw away money. I understand that there may have been a point in time where this was a good deal, but that time has long passed.

There can also be hidden costs to using a particular library or framework that are worth considering. Two that spring to mind are Next.js and React Query. I love both of these libraries, but it's worth thinking for a second about the hidden costs of these libraries. While times are a changing and Next now has many easy deployment options, the blunt reality was that for a long time, if you didn't deploy your Next app on Vercel, you couldn't utilize a lot of the cutting-edge features like ISR. This wasn't really advertised or put out anywhere, so if, for whatever reason, you needed to deploy your Next app elsewhere, this could cause issues or reduce functionality. Luckily, Vercel is wonderful and cheap, at least for any use cases I've used it for, but it's still a hidden cost if you plan to deploy elsewhere.

In a different vein, React Query has its own hidden costs, this time around its default configuration. It's really worth noting that React Query's default refetching time is aggressive. Like really aggressive. It prioritises making sure that data isn't stale more than worrying about whether the fetch is actually necessary. Think for a second about if you're fetching a huge amount of orders or very deeply related data. Obviously, there are design patterns like pagination or infinite scroll to account for this, but still, if this is repeatedly happening several times every few seconds, your costs could explode. Think the egress bandwidth you pay, the number of wasted serverless function invocations, the CPU time on data proxies like Prisma and/or your managed database. All of this RAPIDLY adds up, be careful with these types of hidden costs hidden behind vendor lock in and default configurations, while I've not encountered them in production, I've heard of others where they have, and they can really bite you.

Security

Security is a massive concern nowadays as well, with companies like Snyk offering automated ways to check the security issues of your dependencies. Each dependency you add increases your risk of adding arbitrary security vulnerabilities to your code. Even though most libraries are much better now at preventing basic vulnerabilities and patching their zero days, there are still issues.

I would say that only really worry about security for packages with critical issues and packages with issues related to core packages that handle user data. Think your server framework, middleware, things like that. That slider that has a minor security vulnerability likely isn't worth worrying about, especially if you're a tiny company.

The best thing you can do is just keep your dependencies as up to date as possible and if critical issues aren't getting patched, and they're related to a package that handles user data, maybe remove it. Another thing to keep an eye out for are fake NPM packages. Make sure you're downloading the legitimate package, you can check on their GitHub and website which one is actually theirs. There's a lot of malware disguising itself as a package with one character different.

Responsiveness, Accessibility, etc.

Finally, there are some more ways to think about whether a library is adding more work for you or not that are worth thinking about, but I'm not going to go into too much detail on. Specifically, think about things like: are the components responsive and accessible? Do they automatically add things like aria-labels for accessibility? Do they have keyboard shortcuts? Are they already responsive and help cut out a lot of that work for you? Libraries like MUI are great at doing all of these, but not all are. Some aren't just not great at doing these, but are actively bad. I've used several that just don't provide any responsiveness and some that don't provide aria-labels.

Conclusion

As this should all help you see, there's a lot to consider when adding a new library to your dependencies in modern web dev. While it doesn't include every issue, it should give you a solid foundation. Still, don't panic, I'm not saying you should run every single dependency through a rigorous checklist each time you want to add a slider. It's more that you should just keep in mind the various tradeoffs around adding each one. Overtime you'll build up an intuition for what's worth using and what's not based on the cost.

Hopefully, this all helped you out! If you liked this article, consider subscribing to my RSS feed to get notified whenever I publish new articles. I'm currently working on one about the future of web development going forward that you may be interested in! Thanks for reading!