Micro Frontends — Sharing Modularized Frontend Components
Like moving from monolithic backend applications to microservices, frontend applications can benefit from the same approach.
As this library grew over time by adding more and more widgets its size increased up to a point which was not anymore acceptable to be loadad on a webpage which uses only a small part of the whole library. Therfore we urged to move to a modularized approach.
Benefits from Modularization
There are several benefits of a modularized frontend component setup. Given a sufficiently big consumer base, with such a setup we can benefit from modularized components in various cases.
Modularization allows the components to be actively developed and versioned by its maintainers. Instead of relying on a central party which takes care of reviewing and releasing a single monolithic library, the modularized components can be released and developed quite independently of one another. Therefore the accountability relies within the responsible party for each component.
By sharing the same user authentication, login state and security mechanisms across different components, those logic intensive tasks only need to be developed and maintained once and can be reused by all other components.
The Modularized Components setup allows the drag-and-drop style reuse of all available components while being able to theme and adjust all components to the custom Corporate Identity requirements. Due to the power of the bundling tool theoretically all styles can be extended or sass variables can be overwritten to fit into the consumer’s custom theme.
Also for otherwise static pages the components provide a simple way with which small building blocks can be put together to upgrade a site with interactive user driven components.
The modularized approach massively reduces the asset size by only loading required components instead of the whole monolithic library. The bundling engine even amplifies this effect by removing all unrelated and unused code within the required components by its tree-shaking algorithm.
The setup allows the consumers to define exactly which versions to use of which component, so that the customer of the components is always in control of its state and can rely on it for testing and QA.
For End Users
One can make sure that a component has the same look-and-feel across all its implementations on different pages and provide a consistent user experience for the same task.
The benefits from modularization of frontend components are immense and increase the more they are used. We developed a setup based upon private npm repositories and webpack, which provides shareable components which are still very customizeable.
We separated the monolithic library into more than twenty individual components published on a private npm repository. An additional base package provides the basic functionality like grid, basic components and general styling and typography while other helper packages provide testing and building processes as well as default configuration.
For the build process we made use of the power of the two main building blocks, the private npm repository as package manager and webpack as bundling engine.
Using the power of npm and the components published on a private npm repository, each implementing consumer can now specify exactly which version of which component should be used on which page. npm takes care of resolving all dependencies within the components and its versions. By strictly using semantic versioning we can make sure to keep backward compatibility across all components and consumers.
Webpack takes care of resolving all used components and its assets and dependencies. It provides a development setup and allows to bundle all needed components together with their assets in a production build. Using tree-shaking the code is minimized to only contain the actually used parts and get rid of unused assets and code.
With this approach we were able to reduce the loaded asset size by up to 56% and have a bundling engine much faster and less error prone than the previous gulp setup.
We adapted our functional and unit tests into an integrated testing engine using webpack, webdriver, sinon and mochaJS. With this setup we are now able to run the tests per component, per widget or across all available components together and make sure that all components behave as they should even when bundling them together.
Of course there are also some drawbacks of using this approach. It is a trade-off between increased complexity on the one side and flexibility on the other.
As the component library and its userbase grows, the more important clear communication channels about updates, fixes and new features will get. Also providing a clear process for change requests and visibility of responsibilities is key for great success.
The overhead for managing multiple components in single private npm repositories should not be underestimated. Also the testing effort will increase, due to nearly unlimited possibilities of different versions of components which should play nicely together.
New bugfix versions of components can be released without affecting the other components. Minor and especially major releases with breaking changes must be well-considered as they potentially affect other relying components which then should be updated as well.
The main complexity of modularized components arises by the amount of different components, their versions and the combination of those. Our main learnings are as follows:
- Take care of publishing breaking changes and releasing new major versions — be aware of the impact. Not only will all consumers eventually have to update to the latest version, but all other dependent packages have to be kept up to date as well.
- Communication and visibility is key — make sure to keep an up to date changelog, frequently inform all involved parties on changes and make the components and its maintainers visible by providing extensive documentation.
- Even though npm takes care of dependency resolution, the package dependencies should be chosen wisely, especially when introducing new versions, not well defined dependencies could end up in increased build asset sizes when npm cannot resolve to a single version of a package anymore and thus will include both requested version.
- Run npm dedupe after npm install and check whether multiple versions of the same component are installed.
- Avoid using a global state to avoid conflicts between packages, using the same naming scheme by accident. Prefixing all CSS classes and whatever other global state you have with the package name will save a lot of trouble and debugging time.
As the main goal of the project was to decrease the needed asset size and only include needed components, while preserving the possibility of sharing components on different pages, sharing a unique user experience, the private npm and webpack approach is the perfect fit.
We were able to setup an ecosystem of modularized components together with their documentation, mailing list for updates and test environment as well as a CDN for shared assets.
The shift from the legacy monolithic library to the new modularized library was performed within a year and was well prepared. We could mostly preserve backward compatibility, such that the implementing consumers of the library did not need to adjust their usages of the library. An extensive how to documentation together with a demo project and hands on session helped the consumers to substitute the legacy build system with the new modularized approach.
We and our customer are very happy with this new approach and are convinced that the modularized approach provides the required flexibility, sustainability and maintainability in a professional growing environment.
For further inputs or questions do not hesitate to contact us using the comment form below or by email to firstname.lastname@example.org