Important update: RxJS 5.5 brought us Pipeable Operators that eliminate some of the problems noted below. If you can update to TypeScript 2.4, RxJS 5.5, Angular 5 and Angular CLI 1.5, you should definitely go with Pipeable Operators.
Angular has some third-party dependencies, one of which is RxJS, a library which makes reactive programming very easy to use. The library is obtained as an npm package. In order to use functionality from the RxJS library, it has to be imported first. So before you can use an operator such as map
in the following snippet, it has to be imported:
route.params .map(params => params.id) .subscribe(id => console.log(id));
Whereas IDEs such as WebStorm (prior to 2017.2) or Visual Studio Code do a good job for auto-importing symbols, they don’t suggest anything at all for RxJS symbols. Both IDEs simply print the following error message from TypeScript:
Property ‘map’ does not exist on type ‘Observable’.
This is due to the nature of how RxJS is bundled: all operators are provided as a single “polyfill” module that adds the operator by monkey-patching the Observable’s prototype. The same applies to static methods of Observable. In order to use the operator both during compile and run time, we have to import the accompanying polyfill module first. TypeScript has an import
directive which is used in different ways: when immediately followed by a string, it executes (“requires”) the external script and imports its typings, but no symbols. To make the above snippet work, you have to import the operator(s) in question at least once—somewhere in your application’s codebase, as it simply augments/polyfills the Observable that is already there:
import 'rxjs/add/observable/of'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap';
Tree Shaking to the Rescue?
Angular CLI also offers a production build that can be triggered by ng build --prod
. In this mode, further optimisations take place, such as Ahead-of-Time compilation, dead code elimination or Tree Shaking. The latter reduces the size of the originating bundle:
A tree shaker walks the dependency graph, top to bottom, and shakes out unused code like dead leaves in a tree. 🌳
Hence, I asked myself if it’s possible to use a more simple or lazy approach and simply import the whole library at a centralised location, such as the Angular app’s root NgModule:
import 'rxjs/Rx';
The benefit of this approach would be that you don’t have to remember importing any operator or other functionality in the future again. The downside is that RxJS is a comparably large library. But thanks to Tree Shaking, this shouldn’t be a problem, right?
Comparing Two Extremes: Importing a Single Operator and the Whole Library
So let’s have a comparison of the two approaches. I created a new Angular CLI project using Angular CLI 1.2.6, Angular 4.3.2 and RxJS 5.4.2. I used a single map
operator as shown in the snippet above and tried both methods: importing the whole RxJS library and importing the single operator only.
At first, I compared the build duration and size of the outputs of Angular CLI’s development build. Here, importing the whole library should have a massive implication on the size of the build output. Note: The output sizes include source maps; the build duration averages out my few tries on my Mid 2015 MacBook Pro.
Development Build | Output Size (bytes) | Build Duration (ms) |
---|---|---|
— | 4,836,599 | 5356 |
rxjs/add/op/map | 5,523,177 | 5975 |
rxjs/Rx | 6,866,427 | 7241 |
As you can see, the build takes almost two seconds longer. The output size is significantly larger, roughly two megabytes in size. But now let’s have a look at the statistics of the production build which includes Tree Shaking:
Production Build | Output Size (bytes) | Build Duration (ms) |
---|---|---|
— | 291,004 | 6311 |
rxjs/add/op/map | 381,348 | 7346 |
rxjs/Rx | 544,420 | 9503 |
Wow, the build using the “lazy method” (importing everything) is 163 KiB (43 per cent) larger than the single-operator import build. The build takes approximately two seconds longer. Unfortunately, Tree Shaking can’t help here as well, again due to the nature how RxJS is bundled: Tree Shaking relies on the symbol names of export and import definitions. As the RxJS operator modules don’t export anything, but change global state (“side effects”) instead, it’s not possible for the Tree Shaker to detect which operators are in use. As a result, all operators (and static methods of Observable) will be included in the build.
Operators Should Be Imported at a Centralised Location
Importing all operators significantly enlarges the build output and increases the build duration. So it seems better so import the operators in use only. In addition, due to the polyfill-ish nature of the RxJS modules, it’s totally enough to import an operator (or static method of Observable) once per codebase. These imports should happen at a single, centralised location.
Why? WebStorm starting from version 2017.2 will allow you to auto-import augmentations such as RxJS’s operator modules. However, WebStorm will offer you this auto-import only once per codebase. The import will be placed in the current file and is valid for all the other files in your codebase. This leads to the problem that when you delete this specific import in the given file, it may break other parts of the application.
Of course, you could add the import statements to other files using that operator as well, but it the IDE won’t prompt you to add the imports there, because they already (globally) exist. In addition, this method will increase the output size by a little, because of the (useless) duplicated import statements that won’t have any effect. Don’t repeat yourself. Furthermore, IDEs (currently) can’t help you to detect if there are unused import statements, i.e. if you import an operator, but don’t use it in a file (maybe because it was deleted in the meantime).
Many projects have chosen to create a file named rxjs-imports.ts at root-level which contains all the imports used throughout the application. This makes multiple imports in specific files unnecessary and solves the problems noted above.
rxjs-tslint-rules Enforces Importing RxJS Operators Correctly
However, this alone does not prevent other developers from placing imports in other files and/or helps detect if there are any unused operators in the codebase. Here’s where a really cool npm package called rxjs-tslint-rules comes in.
Just add this package to your project and extend your tslint.json as follows:
{ "rules": { // … "rxjs-add": { "options": [{ "allowElsewhere": false, "allowUnused": false, "file": "./src/rxjs-imports.ts" }], "severity": "error" } // … }
This plug-in for code-style checker tslint makes sure that RxJS imports are placed in a given central file (line 8) exclusively (line 6) and can additionally detect if this file contains imports that aren’t used throughout the application (line 7).
In my opinion, currently this is the best way to import RxJS operators (or static methods of Observable) within your codebase.