Separation of concerns is one of the key principles in software design, and since each entity in the code has to be isolated from others, it makes sense to put them into separate files to simplify the navigation and ensure isolation.
Modern JS applications consist of modules that can have exports and imports. JS modules export some entities and may consume exported entities from other modules.
In this book, we will use the latest JS syntax with classes, arrow functions, spread operators, and so on. If you are not familiar with this syntax, you can always refer to it here: http://exploringjs.com.
The simplest JS module looks like this:
// A.js:
export const noop = () => {};
This file now has a named export, noop, which is an arrow function that does nothing.
Now in B.js, we can import a function from the A.js file:
//B.js:
import {noop} from "./A.js";
noop();
In the real world, dependencies are much more complex and modules can export dozens of entities and import dozens of other modules, including those from NPM. The module system in JS allows us to statically trace all dependencies and figure out ways to optimize them.
If the client downloads all JS in a straightforward way (for example, initially downloading one JS file, parsing its dependencies, and recursively downloading them and their deps), then load time will be dramatic, first of all because network interaction takes time. Secondly, because parsing also takes time. Simultaneous connections are often limited by browser (the amount of HTTP threads) and HTTP 2.0, which allows us to transfer many files through one connection, is not yet available everywhere, so it makes sense to bundle all assets into one big bundle and deliver them all at once.
In order to do this, we can use a bundler like Webpack or Rollup. These bundlers are capable of tracing all dependencies starting from the initial module up to leaf ones and packing those modules together in a single bundle. Also, if configured, they allow us to minify the bundle using UglifyJS or any other compressor; this reduces the bundle size dramatically. Minification is a process where all unnecessary things are stripped out of the bundle, such as whitespaces and comments, all variables are named a, b, and so on, and all syntax constructions are simplified. After minification, we can also gzip the output if the server and client allow this.
But the bundle approach also have caveats. A bundle may contain things that are not required to render a particular requested page. Basically, the client can download a huge initial bundle but in fact need only 30-40% of it.
Modern bundlers allow us to split the app into smaller chunks and progressively load them on demand. In order to create a code split point, we can use the dynamic import syntax:
//B.js:
import('./A.js').then(({noop}) => {
noop();
});
Now, the build tool can see that certain modules should not be included in the initial chunk and can be loaded on demand. But on the other hand, if those chunks are too granular, we will return to the starting point with tons of small files.
Unfortunately, if chunks are less granular, then most likely they will have some modules included in more than one chunk. Those common modules (primarily the ones installed from NPM) could be moved to so-called common chunks. The goal is to find an optimal balance between initial bundle size, common chunk size, and the size of code-split chunks. Webpack (or other bundlers such as Parcel or Rollup) can optimize it a bit, but a certain amount of manual tuning is required for best results.