Microfrontend Pattern Comparison
The desire to modularize the frontend has steadily increased in the last year. One of the challenges for everyone implementing microfrontends is that there is no single pattern to follow. The available architecture patterns are quite different and come with their advantages and challenges.
Usually, each pattern has a distinct area of use even though hybrids and complex mixtures of the available patterns can be used to blur these boundaries as desired. In this article, I want to go briefly over four of the most common microfrontend patterns to understand their main pros and cons.
The presumably simplest, but most reliable integration is the build-time integration. Reliable in the sense that at build-time we know already how everything works and we can join the different pieces to get a single deliverable.
This kind of mechanism is as old as writing software is. In the end, quite often different pieces have been developed independently at different locations, just to arrive at a single point for final assembly. To say the least, automation is key here. The process is best when it just triggers autonomously when any piece changes.
For instance, when a single microfrontend changes, the whole application should just be rebuild. Since the number of microfrontends may grow indefinitely this can be a lot of stress on the build server. Even if not, constant refreshes of the whole application may prevent caching which forms the basis for great SPA performance.
- Type checking
- Runtime optimizations
- Easy for migration
- Dynamic loading
- Build times
The build-time integration works great in combination with server-side integration or for smaller applications where only some well-defined parts are outsourced. One possible solution here is to use Webpack with the module federation plugin.
We can choose this pattern quite easily if we have an existing application that should now be extended with some components provided by other teams. This leaves them quite some freedom without having to re-architecture the whole application.
Moving on to the server-side integration this integration is our weapon of choice for anything dynamic that should also be served server-side rendered. This method will excel in perceived performance.
There are various ways of realizing server-side composed microfrontends. Using a layout engine such as podium we can quickly find a scaling approach without too much trouble. On the other hand, the dynamics of microfrontends may be difficult to tame with a central layout engine. Here, approaches such as using a reverse proxy could be more fruitful.
The challenge of using a reverse proxy is that the local development setup becomes rather complicated. Quite often, the only possibility of actually seeing the integration live is to deploy the microfrontend in question or to provide some hot loading capability for sustaining a local development environment.
- Best performance
- Dynamic loading
- Framework integration
- Microfrontend isolation
- Development environment
The server-side integration works great for content-heavy sites. One example where this pattern shines is webshops. It also provides a great basis for progressive enhancement, which also performs some JS enhancements if possible.
Through techniques such as frames, SSI, or ESI the server-side integration of different frontend parts has always been possible. With new frameworks and techniques, everyone can build quite complex applications in a distributed fashion, which are joined on the server.
Run-Time Integration via iframe
One exception is the inclusion of iframes. This can already be prepared on the server-side quite well, however, requires single elements including their purpose and area of use to be known centrally.
The best part about iframes is their isolation. This also beats alternatives such as shadow DOM or CSS modules as indeed nothing is shared with the hosting application. Since iframes come from a dynamic source their content can be server-side rendered, too. This is also necessary to some degree, as resources cannot be shared and need to be loaded multiple times.
- Strong isolation
- Full flexibility
- No sharing possible
- Difficult to sustain great UX
- Worst performance
The run-time integration via iframes works great for pages using third-party content, where strong isolation is required. This technique is already applied for ages. The first on-site PayPal integrations used it. Many chatbots and consent solutions use it. The reason is that the provided boundaries are just great to shield one application from another.
If a more seamless UX or transition is required, this technique could be replaced by the direct inclusion of a script carrying the microfrontends information.
Run-Time Integration via script
For the run-time integration of microfrontends, a plugin mechanism can be utilized, too. This method has the advantage that everything could be built very easily choosing all the right parameters centrally. The central location is usually called the application shell, or abbreviated "app shell". It loads the scripts and evaluates their content.
While some frameworks offer great control over the distributed API, others are only script loaders or basic routing engines. Nevertheless, pretty much all solutions in this space focus on developer experience. Being productive and able to develop, debug, and ship new microfrontends quickly is certainly a useful attribute.
- Very dynamic
- Super flexible
- Best developer experience
- Weak isolation
- Efficient orchestration
This approach should not be underestimated. It can give great flexibility but comes at some costs. Interesting applications such as VS Code have been built using a plugin system, which proves that a combination of a powerful app shell that comes with the majority of the UI is as viable as a weak app shell that only orchestrates the different microfrontends.
Alternatively, the integration via script can also bring microfrontends in form of web components. While this approach does have some loyal followers, it also comes with additional challenges - mostly in the backward compatibility sector.
As written there is no clear winner. It all depends on the anticipated use case and project characteristics. Hybrid solutions may be able to represent the sweet spot. However, the additional effort for developing (and maintaining) these hybrids should be factored in, too.
When aiming for a hybrid solution it still makes sense to start with one of the presented patterns first. Having a clear roadmap in mind and avoiding over-engineer the solution, in the beginning, is crucial to come out with a usable application in a well-defined timeline.