The JS library distribution dilemma: NPM or URL?
Is NPM always better than a simple link to distribute your JS library to third parties?
I share a few suggestions on how to develop your frontend library to ease its distribution to partners/customers, and why we chose to distribute most of our libraries using CDN instead of NPM.
Website performance is very important and the heavier a JS library is, the worst impact it will have on performance. A careful and conscientious developer will discard all libraries that greatly affect page weight.
It is not easy to convert the “very heavy” statement into a specific number of kilobytes, because it depends on the project and its performance budget.
The library architecture
To reduce the performance costs of our libraries, we implemented them as a series of smaller and independent modules. Each module covers a specific role within the library.
Our libraries always have a bootstrap module, a very light module (we’re talking about a few KB) which just contains the minimal business logic: it implements the init library method.
- the aforementioned “bootstrap module”, which size is mostly influenced by the Polyfills needed to support IE11;
- the “Component module”, it exposes all UI components;
- the “Tracking module”, used by both the modules described above: it records the user interactions.
In the diagram above, we show the Thron.js loading flow. If for example, you want to call a method, only bootstrap, API and tracking modules will be downloaded.
All modules are loaded asynchronously with the rest of the page (the script will be executed while the page continues the parsing) to have little or no impact on page performance.
We focus on just two points when deciding if and how to break up a library:
- Balance the number of modules and their weight; if the module is too heavy (in our case higher than 500KB not gzipped), consider splitting it into smaller modules;
- Avoid overdoing the number of modules. Many modules of just a few KB can be a disadvantage rather than a benefit, because HTTP and network overhead will be greater than the loading time for small files.
The second point can be quite tricky to manage, consider the following example: suppose we need to implement a UI components library composed of a button, checkbox, select input, and text input. If we decide that each element is a separate module, consider what happens when your users want to build a simple web form for a webpage using all UI elements from the list, 5 different HTTP requests will be needed to load all the modules (one for bootstrap and one for each UI element). That’s a lot of connections that will be opened towards the same domain (most likely) and they will count against the browser limit: all browsers implement a limit of concurrent connections against the same domain to improve page loading speed while preserving system resources. Your increased connection count will affect page performance instead of improving it.
Distributing your library to partners and customers
In an ideal world, distributing the library via NPM has only advantages because the customer/partner developers will be ready to update their version, fix the broken code (if any) and deploy the new, better, version. Unfortunately, this does not happen often: sometimes you have a customer who relied upon an external agency to develop and the whole deployment chain is just too long. Of course, we cannot (and will not) take charge of any fix, but the ability to hotfix our bugs without having to wait for the whole deployment chain to run is, sometimes, a welcome addition.
A web project, unless extremely simple, is usually built using a module bundler (for example Webpack, or Parcel).
When this happens, the library is no longer “in your hands”, but is included, encapsulated within the applications that use it.
Asking to update to the new version to its customers/partners, is not a trivial request: the customer/partner, must understand what are the projects impacted, understand who implemented the project (may have been done by an external consultant), understand if you can update it … in short, it is definitely a nuisance for our partners/customers that we want to avoid!
This choice of ours is also supported by many other important brands (Google, Facebook for example). Here are some examples of libraries released through CDN
- Google Maps API: https://maps.googleapis.com/maps/api/js
- Google OAuth implementation: https://apis.google.com/js/api.js
- Facebook js API: https://connect.facebook.net/en_US/sdk.js
- Twitter API: https://platform.twitter.com/widgets.js
There is a risk of introducing regressions when fixing the library, especially if it’s not being included in code we know. We mitigate this risk by providing versioning of the library files: as you might have noticed already, we use a version number in the library include path, this allows us to “bump” the library version any time we are aware that some regression MIGHT happen, this helps our customers/partners to retain full control about library upgrades, as it would be if they were using NPM.
The endpoint structure
The endpoint of our libraries, always point to the bootstrap module (the library entry point, explained above) and follows this structure:
https://<companyId>-cdn.thron.com/shared/lib/common/<libName>/<libVersion>/<assetName>.js , where
- <libName> corresponds to the library Name
- <libVersion> corresponds to the specific library Version
Adding the library version in the URL is very important to avoid regressions; if we were not using this versioned URL, a scenario like the following one might happen:
- We release “lib/wonderfulLib/boot.js” library
- A Partner adds the library in its project and uses the library method “embed”
- Our team decide to improve the method “embed” modifying it. Unconsciously the changes breaks the partner implementation
- The library is released in production, and “lib/wonderfulLib/boot.js” points to the latest version of the library
- The changes are propagated in the partner’s project…
Two rules should always be kept in mind:
- Each library version must always have the corresponding URL
Is modular architecture with CDN-publishing always the best choice?
Our rule of thumb to decide whether to distribute the library through CDN or NPM is simple:
- is the library going to be open source? If so, use NPM;
- is the library targeted to company’s internal developers? If so, use NPM;
- is the customer/partner willing to accept the task of managing library updates in ALL cases? If so, use NPM;
- In any other case use CDN.
A library delivered via NPM is easier to install on a project that uses a module bundler (such ash Webpack or Parcel): we use NPM for our internal libraries or those libraries that are used just as dependencies.
To answer the initial question, the answer is NO; as usual, you should pick the right tool for the job: we love NPM and we use it where possible!
How do you manage JS library distribution? Let us know if you disagree with our approach and why you do: we’d love to discuss it.