With CRA, out of the box the end result is going to have 2 direct dependencies (React and ReactDOM)
- React has 3 dependencies:
-
loose-envify
for process.env vars, which is tiny, and which in turn has 1 tiny dependency.
-
object-assign
, which is a tiny Object.assign
polyfill for older browsers.
-
prop-types
, which 3 dependencies: the 2 dependencies above + react-is
which is tiny.
- ReactDOM has 4 dependencies:
- the 3 above
-
scheduler
, which has 2 dependencies (loose-envify
and object-assign
)
So in total, 8 dependencies, of which React and ReactDOM are the only large-ish ones (they’re still pretty small). Basically, a few hundred kilobytes of code (a few tens once optimised).
All the rest is tooling to allow you to build the code. That’s most of the stuff you can see in node_modules. JavaScript as a language has no built-in tooling, so everything has to be a library. With CRA, most everything is built on top of the module bundler Webpack, with Babel (that, critically for React, can turn JSX to JS), and ESLint to find errors in the code. CRA needs to be able to handle most scenarios, so for example it lets you import CSS files or SVG files in your code even though those aren’t JS files – there has to be the tooling in place to let you do that. And all of the tools are modular, so there is normally a core package then lots of packages that provide the various functionalities needed. Lots and lots of packages (this isn’t a bad thing btw, it’s just a tradeoff, and a good tradeoff at that). Here’s an overview:
https://levelup.gitconnected.com/what-does-create-react-app-actually-do-73c899443d61
So it can:
- build your React app JS code using your code + the react and react dom libraries
- convert that to code that will run in older browsers even if you’re using newer features.
- build an optimised version for production.
- build the HTML index file it’s going to be rendered to
- take CSS files, allow them to be used as modules in the code but split them out into a single output CSS file
- add in any browser-specific prefixes needed to the CSS to allow it to work in all browsers.
- run a development server that lets all this happen locally
- set up the test runner (Jest) with the assertion libraries and test framework that let you start writing tests immediately.
- include setup for a service worker that will allow the app to work offline.
- set up linting to make sure errors get automatically caught as you’re developing.
- include a11y linting to help highlight accessibility issues that might need fixing.
- full support for Typescript with a flag at setup.
- full support for Flow with a flag at setup.
- full support for plug’n’play (Yarn, doesn’t use node_modules).
So this all works pretty well, but it’s fragile. It’s a load of complex tools glued together with some scripts. And generally more tools get dumped on top of that as you try to do more things. So it’s more than reasonable to worry about the size and the ability to understand it all: lots of people are worried about similar things.
So state of the art & current bleeding edge (sorry, massive wall of text) :
node_modules itself is a bit of an issue, as is the explosion of dependencies that can happen. The Yarn package manager doesn’t have node_modules and is very good at deduplication: this works well in practice (CRA for example goes from about 250Mb of node modules to about 50Mb of cached zip files). The packages are still there, but the size reduction means it’s feasible to commit everything to a repo (which in turn removes any install steps). I think things will move toward the Yarn model, but there’s also pnpm that’s been mentioned (uses a global cache afaik, so you only install a given dependency once on a given machine). I think Yarn will win out, or at least how it works will be absorbed into NPM and node_modules will disappear as a thing, but maybe not .
They don’t really reduce the dependency complexity in any way: all the dependencies are still there, just in a different place.
One of the main issues with React is that, because it uses JSX, it needs a compilation step to turn the JSX into JS. This can be done in-browser via (eg) Babel, but that’s not feasible IRL because it’s too slow. React as a library isn’t designed to run in a browser either – FB don’t produce ESModule builds, which are necessary to be able to do that, it is assumed there will always be a build step.
Preact, which shares the same API as React, can do that, and can also use a library called HTM to write components (it looks like JSX, but uses JS template strings). This does have constraints (particularly w/r/t adding dependencies), but it works pretty well. You technically don’t need node_nodules at all if you do this, you can just write JS and import the stuff you need in the JS files from a CDN like Skypack or Unpkg or JSDeliver. See Preact’s documentation. Note that the Vue UI library can also do this (and note that one of the highlighted usecases is simply replacing code that would have used JQuery with code that uses Vue).
Generally, though, you do want to bundle your JavaScript unless you can guarantee that the client has a very modern browser, that you don’t need much in the way of extra libraries, and that you’re happy losing out on a lot of the optimisation that a bundler can do.
Webpack is the de facto standard tool for building JS applications. It’s modular, and the ecosystem of plugins is now huge. Just out of the box it actually works pretty well, but it’s extremely flexible, and that flexibility means it’s often not at all simple. It’s also pretty slow.
Parcel is an alternative: it takes a lot less config but the trade-off is that it’s doing basically the same things, but now they’re magic and hidden. Also pretty slow.
Rollup is another alternative, but it’s designed to produce es modules (which it is very good at doing), and as such it’s primarily for bundling libraries rather than applications (it becomes as complicated and unwieldy as Webpack if you try to use it as an app bundler). Somewhat faster than the other two, but not really targeted at general application use.
Snowpack wraps the client-side code up and serves it as ESModules, which makes iterating in development incredibly fast compared to any of the above. It seems kinda jerry rigged atm though and a bit flaky, docs are not great.
ESBuild is slightly limited at the minute (not necessarily a bad thing), but generally works really well if it’s just JS (no CSS etc). It’s between 10-100 times faster in terms of compilation, and it replaces lots of dependencies with a single binary written in Go. It’s very good, and completely usable, but it’s incomplete atm compared to say Webpack (it can’t completely replace it anyway, but it’s only trying to do that for common usecases).
The Rome build tool is attempting to cover formatting, linting, bundling, etc (ie almost everything those 250Mb of development dependencies do in a single executable), it’s way, way off at the minute though (only works for linting atm). More modern languages (Go, Rust and Elixir for example) ship with tooling. Having a single tool that does this for JS would be very nice, and would solve innumerable headaches.
Deno (an alternative to Node) has the capacity in the future to work as a platform for JS app build tools, particularly as code written with Deno should often Just Work in a browser as-is. But it’s extremely new and can’t do any of the optimisation that a bundler can do (it doesn’t need to, its server-side).
Also
- Babel, which is a JS -> JS compiler, is kinda becoming less necessary with evergreen browsers become the norm (ie they automatically update). It or something similar is still needed for JSX or GraphQL or other things that can be compiled to JS code, and for newer features, and for converting modern code to older versions of JS (there are some alternatives, SWC for example).
- Things will move towards ESModules (so less need for bundling, plus HTTP/2 in theory works better with lots of small files loaded in parallel rather then one or two big bundles) but that’s a way off as there quite a few hurdles to get over. Bundlers can “tree shake”, ie remove bits of code that aren’t used from the end bundle, there’s no solution to that with ESModules atm (this is a core issue with attempts to use Deno for building FE code as well, as Deno works the same way).