(And by “we” I mean “our Frontend Development team at Seznam”. Hi, folks! Anyway, moving on…)
Microfrontends, also known as frontend microservices, are what happens when Frontend Developers look over the fence at the Backend Developers and think: “wait, can we get some of that too, please?’’
The idea goes something like this:
- split your frontend into self-contained sections (or components, or widgets),
- establish an input/output system for the sections to communicate with the backend and each other,
- build a composition layer that will put your bricks together and make them play together nicely.
If, after reading this, you’re asking “but why?”, the answers are similar to backend microservices: easier debugging of a smaller codebase, reusing components across projects, smaller developer teams, to name a few. That said, microservices are hardly a magic bullet. They do bring another level of complexity, something that might not be worth the potential benefits.
Doing our research, one thing became clear very fast: since microfrontends are a vague concept, the actual implementations vary wildly. Florian Rappl in his article 6 Patterns for Microfrontends does a good job categorizing microfrontend approaches. At first glance, the options seem endless.
On closer look, though, many of the mentioned solutions are incompatible with our existing architecture. We can’t use nginx to embed our microfrontend (a solution outlined by the Micro Frontends project), we don’t have access to GCloud or Amazon S3 (OpenComponents), we can’t rebuild our sites to be completely split into microfrontends (Piral). We also need server-side rendering, which disqualified a large portion of otherwise compatible technologies.
In the end, no existing framework did just what we needed. Therefore, Merkur.
A bit of good old do-it-yourself
You can see a Merkur widget in action in this live demo. To take a look at how we did things, run:
npx @merkur/create-widget my-first-widget
This is a starter script that generates all basic scaffolding to build and serve a Merkur widget. What it gives you is Merkur itself (@merkur/core), a method to generate the HTML both server- and client-side (you get a pick from several libraries like Preact or Hyper), and a way to serve the widget (currently Express). Both the HTML renderer and server can be swapped for something else — you can have a Merkur widget built with Vue and served by Fastify, if you like.
Merkur’s approach is similar to the one outlined in Zack Jackson’s article Micro-frontend Architecture: Replacing a Monolith from the Inside Out. The whole lifecycle looks something like this:
- The host application makes a call to the Merkur widget API during its server-side rendering phase. It supplies all props for the initial render of the widget.
- The widget is initialized and runs its own server-side rendering phase.
- The API returns the widget as an object with all its data — name, version, props, widget state, and crucially, style/script asset URLs and rendered HTML.
- Host application includes the scripts, styles and HTML in its own server-side render.
- In the browser, the host initializes and mounts the widget. Because the widgets are named and versioned, it’s possible to run several widgets side by side.
- The widget now “lives” within the host app. Two-way communication is possible by changing props (host => widget) and either native DOM events, or custom Merkur events on the widget (widget => host).
Server-side rendering is optional, by the way. Most things in Merkur are; the core is minimal, everything that isn’t necessary for basic functionality moved into a plugin.
Report from the trenches
So far, we have built 4 widgets at several levels of complexity. We need more time to judge how well the positives and negatives stack in production, but development-wise, it’s been a pleasant experience.
First, Merkur widgets are tiny. Compared to our usual codebase, it’s a joy to debug something that’s barely 15k lines of code, including the library!
The small size also allows for experimenting. There should be some limits — I can’t imagine a bigger nightmare than maintaining an army of widgets where every single one uses different technology — but we picked a few things to try out. Not all of these will get ported into our applications (we’d rather not mix hook-heavy functional React components into our largely class-component-based projects, for example), but some of them will (migrating our build process to Webpack is definitely on our to-do list).
Not everything has been sunshine and unicorns, obviously.
Building a custom tool means actually building it — anything from a bugfix to a complex feature, we had to come up with our own solution. We hadn’t run into a true show-stopper, but there have been a few nasty surprises.
Beside Merkur-specific issues, we faced problems stemming directly from the microfrontend architecture. The downside of encapsulated units is increased complexity of the system as a whole. In shorter words: the bits are simple; putting them together… not so much. It comes up the most when debugging. Where you had one application to monitor for error messages and search for bugs, you now have two. Add a few layers of shared integration modules (inevitable when we need to use one widget on several sites), and things can get hairy.
Generally though, we’re optimistic. Some initial problems are inevitable, and nothing we’ve encountered so far has made us doubt our approach. Microfrontends in general, and Merkur in particular, are tools Seznam will continue to use in the future.
If you’re interested in Merkur, we plan to publish a case study of our last Merkur project, data visualization widgets for the 2020 Czech senate and regional elections. Meanwhile, check out the documentation and the GitHub page — Merkur is OpenSource and we welcome contributions, both code and bug reports.