The “Invisible Cost” We Download Every Day
Developers usually start their work with a single command: npm install. This one line does an astonishing amount of work. It downloads the required libraries, resolves dependencies, and prepares the project in an executable state. Because this process is so natural and fast, most developers do not think deeply about what is happening behind the scenes. However, hidden behind this simple command is the fact that dozens, sometimes thousands, of packages are downloaded together. And a significant portion of them are code that we neither directly use nor are even aware of.
The problem is that these pieces of code do not simply “take up space.” Dependencies affect installation time, network cost, and disk usage, as well as runtime performance and memory consumption. Furthermore, from a security perspective, they expand the attack surface, and from a maintenance standpoint, they exponentially increase debugging complexity. In other words, even though we did not write the code ourselves, the system gradually becomes more complex, slower, and more vulnerable. This cost is more dangerous precisely because it is invisible. Developers, seeing that “everything works fine,” continue to accumulate it without recognizing its impact.
This phenomenon appears in an especially extreme form within the modern JavaScript ecosystem. The culture of installing a separate package to implement even a small feature, combined with the structure in which those packages depend on yet other packages, naturally creates a deep dependency tree. As a result, it has become common for dozens of indirect dependencies to be included for a single feature. The critical question here is: are these pieces of code truly necessary? In reality, however, this question is rarely asked. Developers focus on implementing functionality, and dependencies are merely consumed as tools. As a result, we build systems by downloading code whose necessity has never been verified.

At this point, a natural question arises. Why have we ended up with so many dependencies? Is it simply because developers are lazy, or because the JavaScript ecosystem is inefficiently designed? To understand this problem, we must first examine how this structure came to be, rather than jumping straight to criticism.
Why Do We Have So Many Dependencies
It is easy to criticize the current JavaScript ecosystem. There are too many packages, dependency trees are bloated, and even small features have their own libraries. However, dismissing this phenomenon as simply “bad design” is not an accurate approach. The current structure did not emerge by accident; it is the result of highly rational choices made under specific historical conditions. Without understanding this, we miss the essence of the problem.
The JavaScript environment of the past was fundamentally different from what it is today. Browsers implemented things differently, standards evolved slowly, and there was no guarantee that the same code would run consistently across all environments. Before ES5, even basic array methods were often missing, and object manipulation capabilities were limited. Node.js, in its early days, was not the stable runtime it is now, and multiple versions coexisted. Under these conditions, developers had a clear choice: either implement functionality themselves or break it into small, reusable packages that could be shared.
This is where polyfills and small utility packages emerged. Polyfills were essential tools that allowed the same code to run in environments where certain features did not exist. It was also a reasonable decision to create wrappers that protected against mutations of the global object or accounted for various execution environments to ensure safe behavior. Through this process, a structure aimed at maximizing reusability and compatibility was formed, naturally evolving into a model of small, modular packages.
What is important is that all of these choices were optimal at the time. Developers had to provide a stable experience to as many users as possible within constrained environments, which meant prioritizing generality and compatibility. As a result, the JavaScript ecosystem evolved toward the goal of “code that works in any environment,” and along the way, a dependency-driven structure became established. This structure worked successfully for a long time and became the foundation of today’s rich library ecosystem.
However, the problem begins here. The environment has changed, but the structure remains. Modern browsers support largely the same standards, and Node.js now provides stable LTS versions. Many of the compensatory mechanisms that were once necessary are no longer essential. And yet, we continue to write code in the same way and accumulate dependencies in the same way. In other words, past rationality is being transformed into present inefficiency.
At this point, it becomes necessary to examine more concretely how this structural inertia manifests and what kinds of costs it actually produces.
Axis One — A Structure Where the Present Pays for the Past
The first key axis of JavaScript dependency bloat is clear. It is a structure in which the present pays the cost to support the past. This goes beyond merely maintaining old code; it leads to unnecessary logic being included in actual execution paths. Representative examples include support for legacy runtimes, protection against global namespace mutation, and handling of cross-realm values. Each of these is essential in certain situations, but in most modern applications, they are rarely used.
For example, polyfills and utility functions created to support ES3 environments are effectively unnecessary by today’s standards. Modern browsers and Node.js already provide ES5 and beyond as a baseline, with array methods and object manipulation functions implemented natively. Despite this, many packages still include code that accounts for these outdated environments. This is not just a matter of increased code size; unnecessary branching and validation logic are introduced into execution paths, affecting performance as well.
Another example is the structure designed to guard against global namespace mutation. Some libraries avoid directly referencing native objects and instead access them through wrappers, assuming the possibility that the execution environment may be polluted. While theoretically safer, this is rarely necessary in real-world applications. Most projects control their execution environments, and arbitrary mutation of global objects is extremely uncommon. Nevertheless, packages containing such defensive logic remain widely used.
The cross-realm problem is similar. To address issues where objects have different prototype chains across environments like iframes or VMs, code is written to check types using more complex mechanisms than instanceof. This is necessary in certain testing environments or specialized execution contexts, but it is almost never encountered in typical web applications or server environments. Yet, this logic is still included in the default implementations of many libraries.
Ultimately, all of these elements converge into a single structure: code designed for a minority of special cases is imposed as a default cost on all users. The most significant problem with this structure is the lack of choice. Even if developers do not need these features, it is difficult to remove code that is automatically included through dependencies. As a result, we continue to pay for functionality we do not need.
At this point, the key question emerges once again. Why has this structure not been removed? And why do we continue to bear this cost? The answer leads directly to the second axis discussed in the next section: is breaking code into smaller pieces always a good thing?
Axis Two — The Illusion That Smaller Is Always Better, Atomic Architecture
If the first axis was “a structure where past necessities remain as present costs,” the second axis is a more contemporary issue. It is the belief that breaking code into smaller pieces is always good design. In the JavaScript ecosystem, the equation “small package = reusability = good design” has long been accepted as an almost unquestioned assumption. At first glance, this philosophy appears highly reasonable. By dividing functionality into small units, it becomes easier to reuse them across projects and compose them into more complex features. However, the problem is that this theory rarely works as expected in real-world environments.
In reality, most small packages do not achieve true reusability. The expectation that a utility created for a specific package will also be used elsewhere often fails. For example, functions that simply convert values into arrays or logic that detects specific platforms are frequently used only within very limited contexts. Although they exist as independent packages, in practice they could be included within a single higher-level package without any issue. In other words, they are not reused because they are separated into packages—they are merely separated.
This structure exponentially expands the dependency tree. As one package depends on multiple small packages, and those small packages in turn depend on even more small packages, the pattern repeats, ultimately forming a deep and highly complex tree. In this process, it is also common for packages with identical functionality to be included multiple times in different versions. What matters here is not the size of the code itself, but the structural cost. As the number of packages increases, so do the costs of network requests during installation, decompression, version resolution, and conflict handling.

The more serious problems emerge in terms of security and maintenance. Even if a package contains only a small amount of code, the moment it becomes a distribution unit, it becomes something that must be managed. Dozens or hundreds of small packages each have their own maintenance cycles and each carries the potential for security vulnerabilities. In particular, when a single maintainer manages multiple small packages, the impact of an attack on that maintainer can be far greater than expected. In fact, such cases have already occurred multiple times. A simple utility function can become an attack vector, affecting not only higher-level packages but also their users.
Ultimately, we return to a fundamental question: did these pieces of code really need to be separated into packages? For simple logic, directly including the code within a project might be a safer and more efficient choice. However, the ecosystem has long accepted “splitting things up” as the correct answer, and as a result, we are now faced with the structure we see today. At this point, the problem is not merely the number of packages, but the fact that a flawed design philosophy has solidified into a standard.
And this philosophy leads to yet another form of problem: code that is no longer necessary continues to remain without being removed.
Axis Three — Ponyfills That Never Disappear, the Solidification of Technical Debt
In the JavaScript ecosystem, the concept of a ponyfill is particularly interesting. While a polyfill modifies the global environment to provide functionality, a ponyfill offers the same capability through imports. In other words, it is a compromise that allows future features to be used without polluting the global environment. This approach was especially useful for library developers. In situations where the user’s execution environment could not be assumed, it allowed them to safely write code while accounting for the absence of certain features.
In this context, ponyfills were clearly a rational choice. The problem, however, is that this structure does not get removed. Even as most browsers and runtimes have come to natively support these features, ponyfills remain in the code. For example, features such as globalThis or Object.entries have been supported across all major environments for years, yet they are still often provided through separate packages.
This phenomenon goes beyond simple code duplication; it represents the solidification of technical debt. Ponyfills were originally designed as temporary solutions, but in reality, they have become permanent dependencies. The reason is clear: removing them is inconvenient and risky. To remove such a package, backward compatibility must be verified, potential breaking changes must be considered, and tests must be rerun. Most maintainers choose to preserve the existing structure rather than bear these costs.
As a result, the problem accumulates. Code that is no longer necessary continues to be included, and the same dependencies are repeatedly used in new projects. Ultimately, the entire ecosystem expands while retaining the conditions of the past. What matters here is that this is not an issue of individual packages. It is a structural problem maintained by the inertia of the entire ecosystem.

In the end, the ponyfill issue converges into a single core insight: we are accustomed to adding code, but not to removing it. And this asymmetry gradually makes systems heavier over time. Combined with the previous two axes, the JavaScript ecosystem naturally evolves into a structure where unnecessary code continues to persist.
When we bring these three axes together, what emerges is not a set of isolated issues, but a clear and consistent pattern.
The Result of the Three Axes — “Invisible System Complexity”
The three axes we have examined so far may appear to be independent problems. In reality, however, they are closely interconnected and converge in a single direction: the growth of invisible complexity. This complexity does not manifest as longer code or more files. Instead, it spreads across the way the system operates, execution paths, and the overall dependency structure, existing in forms that are difficult for developers to perceive.
This complexity first becomes apparent during the installation process. Downloading numerous packages, decompressing them, and resolving dependencies may seem like simple preparatory steps, but they actually require significant resources. As projects grow larger, this cost increases, and the time required to initialize the development environment begins to impact productivity. However, the more critical issue arises at runtime. The inclusion of unnecessary code means that such code exists within execution paths, directly affecting CPU usage and memory consumption.
From a security perspective, the impact is even more severe. As the number of packages increases, the attack surface expands, and the likelihood that a single vulnerability can propagate throughout the entire system also grows. In structures where small packages are chained together, a single vulnerability can spread through unexpected paths. In such cases, developers struggle to trace the root cause, and response times inevitably slow down. In other words, this is not merely an increase in dependencies, but a structural expansion of risk.
Finally, the maintenance perspective introduces even more long-term consequences. As dependencies increase, the likelihood of version conflicts grows, and updates to a specific package can cause unexpected issues. During debugging, developers must traverse the dependency tree to identify the root cause, significantly increasing both time and cost. Ultimately, developers find themselves spending more time solving problems than building features.

When all these factors are combined, we arrive at a single conclusion: we are not simply using a large amount of code—we are injecting uncontrollable complexity into our systems. And because this complexity is invisible, it is even more dangerous. Developers continue to add dependencies without recognizing it, and as a result, systems become progressively heavier.
We are now ready to move to the next step. It is necessary to examine whether this problem is unique to JavaScript or whether it is a recurring pattern across other ecosystems as well.
Comparison with Other Ecosystems — Why Java Faced This Problem Earlier
If we look at the discussion so far only within the JavaScript ecosystem, this problem may appear to be a structural flaw unique to JS. However, when we broaden the perspective, this issue is not new at all. Other language ecosystems have already experienced similar problems, and Java, in particular, encountered them much earlier. Dependency management systems based on Maven and Gradle initially improved productivity dramatically, but over time, their side effects began to surface in the form of dependency hell.
In the Java ecosystem, the problem primarily manifested as version conflicts and classpath conflicts. Different libraries would require different versions of the same package, leading to unexpected runtime errors. Techniques such as shading or relocation were introduced to address this issue—essentially copying libraries internally to avoid conflicts. However, these approaches were closer to workarounds than fundamental solutions. As a result, Java developers gradually shifted toward reducing dependencies themselves or establishing clear versioning strategies.
In contrast, the JavaScript ecosystem evolved in a somewhat different direction. Instead of classpath conflicts, JS chose a structure that isolates packages in a tree-like form to avoid version conflicts. This approach was highly effective in the short term. Since different versions of the same package could coexist, conflict issues were significantly reduced. However, this decision introduced a new problem: duplication and bloat. Because conflicts do not occur even when the same functionality is included multiple times, the need to eliminate redundancy is reduced.

Ultimately, the two ecosystems chose different forms of complexity. Java dealt with complexity centered around conflicts, while JavaScript deals with complexity centered around duplication. What matters is that in both cases, once the concept of dependency exceeds a certain scale, it turns into a structural problem. In other words, this is not an issue of a specific language, but a limitation inherent in the dependency-driven development model itself.
Through this comparison, we arrive at an important insight. The current state of JavaScript is not unique—it is simply another variation of a path that other ecosystems have already gone through. And this naturally leads to the next question: how do these structural problems manifest in real systems?
The Problems We Actually Experience — Performance, Stability, and Operational Cost
Let us now bring the discussion down to a more practical level. If the previous sections were structural and conceptual, this section focuses on the issues that arise in real systems. In environments where high performance is required or stability is critical, dependency bloat is not just an inconvenience—it becomes a direct source of failure.
The most immediately noticeable issue is performance. Having many dependencies ultimately means that more code is included in the execution path. Even if some of that code is not actually used, it still incurs costs during loading and initialization. In server environments in particular, these costs accumulate and can lead to increased latency. For example, if unnecessary modules are loaded during the handling of a simple request, and additional computations are performed within them, this directly impacts TPS. Small inefficiencies, when repeated, become limiting factors for the entire system’s throughput.
From a stability perspective, the problem is equally clear. The more dependencies a system has, the more it relies on external components. If an issue arises in a single package, it can propagate upward through the dependency chain and ultimately affect the entire application. In cases of intermittent errors or bugs that occur only under specific conditions, tracing the root cause becomes extremely difficult. Developers are forced to analyze not their own code, but dependencies several layers deep.

Operational cost is also a significant factor. As dependencies increase, update cycles become more complex, and the burden of tracking changes across packages grows. The same applies when security vulnerabilities are discovered. Issues often arise not in directly used packages, but in indirect dependencies, and resolving them may require reconstructing the entire dependency tree. This extends beyond development cost and becomes an operational cost at the organizational level.
Ultimately, we arrive at a clear conclusion. Dependencies are not merely tools for development convenience—they are core elements that determine system performance and stability. And the current structure treats this element far too lightly. At this point, we can no longer remain at the stage of recognizing the problem. We must begin to consider the direction in which we should move.
Then How Should We Approach It — A Strategy to Reverse the Structure
Through the discussion so far, the problem has been sufficiently exposed. However, recognizing a problem and solving it are entirely different matters. Especially in a massive ecosystem like JavaScript, simple principles alone are not enough to drive real change. Therefore, we need to consider more practical and concrete strategies. The core idea is simple: do not try to support everything—choose what fits your environment.
The first step is a change in attitude toward dependencies. Until now, we have treated packages as if they were “free resources.” In reality, they are not. Every dependency carries a cost, and that cost will eventually be reflected in the system. Therefore, when adding a new package, we must always ask: is this truly necessary? Especially for simple logic, implementing it directly may be a better choice. Writing a few lines of code can be far more efficient than introducing dozens of dependencies.
Second, we must clearly define our target environment. Attempting to support all browsers and all versions of Node is no longer realistic. In modern development, it is more rational to define a clear support range and optimize within that boundary. This allows us to eliminate unnecessary polyfills and ponyfills and significantly reduce code complexity. The key in this process is to clearly define who the code is for.

Finally, the use of tools is important. Analyzing dependency trees, removing unused packages, and identifying replaceable functionality are tasks that are difficult to perform manually. Various tools exist to assist with this, and it is necessary to actively utilize them. However, tools are only supporting instruments. The most important factor is the developer’s judgment. Deciding which dependencies are necessary and which code should be implemented directly is ultimately a decision that developers must make.
This strategy may incur short-term costs. Existing code must be modified, new standards must be applied, and some packages may need to be replaced. However, in the long term, it leads to reduced system complexity and improved performance and stability. Ultimately, we must make a choice. Will we continue to accumulate costs while maintaining convenience, or will we restructure and build a system that we can control?
The difficulty of this choice connects directly to the practical limitations discussed in the next section.
Practical Constraints — Why We Cannot Easily Change
If we follow the flow so far, the direction of the solution appears relatively clear. Reduce unnecessary dependencies, define the target environment explicitly, and maintain the simplest structure possible. However, in real development environments, it is not easy to move in this direction. The reason lies not in technical difficulty, but in structural constraints and practical costs. In this section, we examine why, even when we recognize the problem, we continue to maintain the status quo.
The biggest obstacle is dependence on the existing ecosystem. Many projects already rely on dozens or even hundreds of packages, a large portion of which are connected through indirect dependencies. When attempting to remove or replace a specific package, it is difficult to fully understand the scope of its impact. In large-scale projects especially, even a small change can introduce unexpected bugs, potentially leading to service disruptions. As a result, developers often choose “do not touch it if it works.” This choice is rational in the short term, but in the long term, it solidifies structural problems.
Another practical constraint lies in the maintenance structure of open source. Most npm packages are maintained by individuals or small teams with limited time and resources. Reducing dependencies or removing ponyfills from already stable code is rarely a priority. When there is a risk of breaking changes, maintainers tend to take an even more conservative approach. As a result, the ecosystem as a whole tends to prioritize stability over change, and existing structures remain intact.

The decision-making structure within organizations is also a critical factor. In environments such as finance or large-scale services, stability is the top priority, and any new attempt requires clear justification and thorough validation. It is difficult to remove existing dependencies based solely on the argument that “a lighter structure is better.” Risks associated with changes must be explained, tests must be conducted, and rollback strategies must be prepared. These processes require significant effort, and when there is no immediate visible improvement in functionality, such efforts are often deprioritized.
Ultimately, we are faced with a clear reality: we understand the problem, but the cost of changing it is too high. At this point, it becomes important to acknowledge this limitation. Rather than blindly pursuing an ideal direction, it may be more effective to pursue gradual improvements within realistic boundaries. In other words, this is not a problem that can be solved all at once, but one that requires continuous management and incremental decisions over time.
With these practical constraints in mind, it is time to revisit the core message of this discussion.
Conclusion — Not “Breaking Things Smaller,” but “Reducing Them Appropriately”
If we follow the flow of this discussion, a clear pattern emerges. The JavaScript ecosystem has grown based on structures formed under past constraints, and those structures remained valid for a long time. However, as the environment has changed, those same structures are no longer optimal. We continue to write code in the same way, add dependencies in the same way, and unconsciously bear the resulting costs. The three axes discussed in this article are merely a framework for explaining this structural issue, but in reality, the same phenomenon repeats across a broader scope.
What is important is that this problem does not originate from a specific library or an individual developer’s choice. It is the result of inertia accumulated across the entire ecosystem over time. Therefore, it is difficult to solve through isolated improvements alone. However, doing nothing is not a viable option either. What is needed now is a shift in awareness at a smaller scale. We must begin by re-examining each dependency we use and asking whether it is truly necessary.

The core message of this article is simple: “breaking things into smaller pieces” is not always good design. In today’s environment, “reducing appropriately” may be a more important strategy. Rather than dividing everything into reusable units, we should shift our thinking toward building only what is necessary and removing what is not. This is not merely a change in coding style, but a fundamental shift in how we view systems.
Ultimately, we stand at a crossroads. Will we continue to accept increasingly complex systems in exchange for convenience, or will we consciously simplify structures to build systems we can control? This choice is directly tied not to short-term productivity, but to long-term stability. And it does not begin with large-scale refactoring, but with a single, simple question: “Why am I using this package?”