Tagged: Micro-frontend

Micro-frontends: The path to a scalable future — part 2
26
Mar
2021

Micro-frontends: The path to a scalable future — part 2

Introduction

So in Part 1 of this article, we’ve learned what micro-frontends are, what they are meant for, and what the main concepts associated with them are. In this part, we are going to dive deep into various micro-frontend architecture types. We will understand what characteristics they share and what they don’t. We’ll see how complex each architecture is to implement. Then through the documents-to-applications continuum concept, we will identify our application type and choose the proper micro-frontend architecture that meets our application’s needs at most.

Micro-frontend architecture characteristics

In order to define micro-frontend architecture types, let’s reiterate over two micro-frontend integration concepts that we learned in the first part: (Fig. 1 and 2).

  • routing/page transitions

For page transition, we’ve seen that there are server and client-side routings, which in another way are called hard and soft transitions. It’s “hard” because with server-side routing, each page transition leads to a full page reload, which means for a moment the user sees a blank page until the content starts to render and show up. And client-side routing is called soft because on page transition, only some part of the page content is being reloaded (usually header, footer, and some common content stays as it is) by JavaScript. So there is no blank page, and, also the new content is smaller so it arrives earlier than during the full page reload. Therefore, with soft transitions, we undoubtedly have a better user experience than with hard ones.

Figure 1. routing/page transition types

Currently, almost all modern web applications are Single Page Applications (SPAs). This is a type of application where all page transitions are soft and smooth. When speaking about micro-frontends, we can consider each micro-frontend as a separate SPA, so the page transitions within each of them can also be considered soft. From a micro-frontend architectural perspective, by saying page transitions, we meant the transitions from one micro frontend to another and not the local transitions within a micro frontend. Therefore this transition type is one of the key characteristics of a micro-frontend architecture type.

  • composition/rendering

For composition, we’ve spoken about the server and client-side renderings. In the 2000s, when JavaScript was very poor and limited, almost all websites were fully rendered on the server-side. With the evolution of web applications and JavaScript, client-side rendering became more and more popular. SPAs mentioned earlier also are applications with client-side rendering.

Figure 2. composition/rendering types

Currently, when saying server-side rendering or universal rendering, we don’t assume that the whole application is rendered on the server-side — it’s more like hybrid rendering where some content is rendered on the server-side and the other part on the client-side. In the first article about micro-frontends, we’ve spoken about the cases when universal rendering can be useful. So the rendering is the second characteristic of a micro-frontend architecture type.

Architecture types

Now that we have defined two characteristics of micro-frontend architecture, we can extract four types based on various combinations of those two.

Linked Applications

  • Hard transitions;
  • Client-side rendering;

This is the simplest case where the content rendering is being done on the client-side and where the transition between micro-frontends is hard. As an example, we can consider two SPAs that are hosted under the same domain (http://team.com). A simple Nginx proxy server serves the first micro-frontend on the path “/a” and the second micro-frontend on the path “/b.” Page transitions within each micro-frontend are smooth as we assumed they are SPAs.

The transition from one micro-frontend to another happens via links which in fact is a hard transition and leads to a full page reload (Fig. 3). This micro-frontend architecture has the simplest implementation complexity.

Figure 3. Linked application transition model

Linked Universal Applications

  • Hard transitions;
  • Universal rendering;

If we add universal rendering features to the previous case, we’ll get the Linked Universal Applications architecture type. This means that some content of either one or many micro frontends renders on the server-side. But still, the transition from one micro to another is done via links. This architecture type is more complex than the first one as we have to implement server-side rendering.

Unified Applications

  • Soft transitions;
  • Client-side rendering;

Here comes my favorite part. To have a soft transition between micro-frontends, there should be a shell-application that will act as an umbrella for all micro-frontend applications. It will only have a markup layout and, most importantly, a top-level client routing. So the transition from path “/a” to path “/b” will be handed over to the shell-application (Fig. 4). As it is a client-side routing and is the responsibility of JavaScript, it can be done softly, making a perfect user experience. Though this type of application is fully rendered on the client-side, there is no universal rendering of this architecture type.

Figure 4. Unified application with a shell application

Unified Universal Applications

  • Soft transitions;
  • Universal rendering;

And here we reach the most powerful and, at the same time, the most complex architecture type where we have both soft transitions between micro-frontends and universal rendering (Fig. 5).

Figure 5. Micro-frontend architecture types

A logical question can arise like, “oh, why don’t we pick the fourth and the best option?” Simply because these four architecture types have different levels of complexity, and the more complex our architecture is, the more we will pay for it for both implementation and maintenance (Fig. 6). So there is no absolute right or wrong architecture. We just have to find a way to understand how to choose the right architecture for our application, and we are going to do it in the next section.

Figure 6. Micro-frontend architecture types based on their implementation complexities.

Which architecture to choose?

There is an interesting concept called the documents-to-applications continuum (Fig. 7). It’s an axis with two ends where we have more content-centric websites, and on the other side, we have pure web applications that are behavior-centric.

  • Content-centric: the priority is given to the content. Think of some news or a blog website where users usually open an article. The content is what matters in such websites; the faster it loads, the more time the users save. There are not so many transitions in these websites, so they can easily be hard, and no one will care.
  • Behavior-centric: the priority is given to the user experience. These are pure applications like online code editors, and playgrounds, etc. In these applications, you are usually in a flow of multiple actions. Thus there are many route/state transitions and user interactions along with the flow. For such types of applications having soft transitions is key for good UX.
Figure 7. Correlation of the document-to-applications continuum and micro-frontend architecture types.

From Fig. 7, we can see that there is an overlap of content and behavior-centric triangles. This is the area where the progressive web applications lay. These are applications where both the content and the user experience are important. A good example is an e-commerce web application.

The faster the first page loads, the more pleasant it will be for the customers to stay on the website and shop.

The first example is that of a content-centric application, while the second one is behavior-centric.

Now let’s see where our architecture types lay in the document-to-applications continuum. Since we initially assumed that each micro-frontend is a SPA by itself, we can assume that all our architecture types lay in the behavior-centric spectrum. Note that this assumption of SPA’s is not a prerogative and is more of an opinionated approach. As both linked universal apps and universal unified apps have universal rendering features, they will lay in progressive web apps combined with both content and behavior-centric spectrums. Linked apps and unified apps are architecture types that are more suited to web applications where the best user experience has higher priority than the content load time.

First of all, we have to identify our application requirements and where it is positioned in the document-to-application continuum. Then it’s all about what complexity level of implementation we can afford.

Conclusion

This time, as we already learned the main concepts of micro-frontends, we dived deep into micro-frontend applications’ high-level architecture. We defined architecture characteristics and came up with four types. And at the end, we learned how and based on what to choose our architecture type. I’d like to mention that I’ve gained most of the knowledge on micro-frontends and especially the things I wrote about in this article from the book by Micheal Geers “ Micro frontends in Action,” which I recommend reading if you want to know much more about this topic.

What’s next?

For the next chapter, we will be going towards the third architecture — Unified apps, as this type of architecture, mostly fits the AI annotation platform. We will be learning how to build unified micro-frontends. So stay tuned for the last third part.

Client-side vs. Server-side vs. Pre-rendering for Web Apps
26
Mar
2021

Client-side vs. Server-side vs. Pre-rendering for Web Apps

There is something going on within the front-end community recently. Server-side rendering is getting more and more traction thanks to React and its built-in server-side hydration feature. But it’s not the only solution to deliver a fast experience to the user with a super fast time-to-first-byte (TTFB) score: Pre-rendering is also a pretty good strategy. What’s the difference between these solutions and a fully client-rendered application?

Client-rendered Application

Since frameworks like Angular, Ember.js, and Backbone exists, front-end developers have tended to render everything client-side. Thanks to Google and its ability to “read” JavaScript, it works pretty well, and it’s even SEO friendly.

With a client-side rendering solution, you redirect the request to a single HTML file and the server will deliver it without any content (or with a loading screen) until you fetch all the JavaScript and let the browser compile everything before rendering the content.

Under a good and reliable internet connection, it’s pretty fast and works well. But it can be a lot better, and it doesn’t have to be difficult to make it that way. That’s what we will see in the following sections.

Server-side Rendering (SSR)

An SSR solution is something we used to do a lot, many years ago, but tend to forget in favor of a client-side rendering solution.

With old server-side rendering solutions, you built a web page—with PHP for example—the server compiled everything, included the data, and delivered a fully populated HTML page to the client. It was fast and effective.

But… every time you navigated to another route, the server had to do the work all over again: Get the PHP file, compile it, and deliver the HTML, with all the CSS and JS delaying the page load to a few hundred ms or even whole seconds.

What if you could do the first page load with the SSR solution, and then use a framework to do dynamic routing with AJAX, fetching only the necessary data?

This is why SSR is getting more and more traction within the community because React popularized this problem with an easy-to-use solution: The RenderToString method.

This new kind of web application is called a universal app or an isomorphic app. There’s still some controversy over the exact meanings of these terms and the relationship between them, but many people use them interchangeably.

Anyway, the advantage of this solution is being able to develop an app server-side and client-side with the same code and deliver a really fast experience to the user with custom data. The disadvantage is that you need to run a server.

SSR is used to fetch data and pre-populate a page with custom content, leveraging the server’s reliable internet connection. That is, the server’s own internet connection is better than that of a user with lie-fi), so it’s able to prefetch and amalgamate data before delivering it to the user.

With the pre-populated data, using an SSR app can also fix an issue that client-rendered apps have with social sharing and the OpenGraph system. For example, if you have only one index.html file to deliver to the client, they will only have one type of metadata—most likely your homepage metadata. This won’t be contextualized when you want to share a different route, so none of your routes will be shown on other sites with their proper user content (description and preview picture) that users would want to share with the world.

Pre-rendering

The mandatory server for a universal app can be a deterrent for some and may be overkill for a small application. This is why pre-rendering can be a really nice alternative.

I discovered this solution with Preact and its own CLI that allows you to compile all pre-selected routes so you can store a fully populated HTML file to a static server. This lets you deliver a super-fast experience to the user, thanks to the Preact/React hydration function, without the need for Node.js.

The catch is, because this isn’t SSR, you don’t have user-specific data to show at this point—it’s just a static (and somewhat generic) file sent directly on the first request, as-is. So if you have user-specific data, here is where you can integrate a beautifully designed skeleton to show the user their data is coming, to avoid some frustration on their part:

Using a document skeleton as part of a loading indicator

There is another catch: In order for this technique to work, you still need to have a proxy or something to redirect the user to the right file.

Why?

With a single-page application, you need to redirect all requests to the root file, and then the framework redirects the user with its built-in routing system. So the first page load is always the same root file.

In order for a pre-rendering solution to work, you need to tell your proxy that some routes need specific files and not always the root index.html file.

For example, say you have four routes (//about/jobs, and blog) and all of them have different layouts. You need four different HTML files to deliver the skeleton to the user that will then let React/Preact/etc. rehydrate it with data. So if you redirect all those routes to the root index.html file, the page will have an unpleasant, glitchy feel during loading, whereby the user will see the skeleton of the wrong page until it finishes loading and replaces the layout. For example, the user might see a homepage skeleton with only one column, when they had asked for a different page with a Pinterest-like gallery.

The solution is to tell your proxy that each of those four routes needs a specific file:

  • https://my-website.com → Redirect to the root index.html file
  • https://my-website.com/about → Redirect to the /about/index.html file
  • https://my-website.com/jobs → Redirect to the /jobs/index.html file
  • https://my-website.com/blog → Redirect to the /blog/index.html file

This is why this solution can be useful for small applications—you can see how painful it would be if you had a few hundred pages.

Strictly speaking, it’s not mandatory to do it this way—you could just use a static file directly. For example, https://my-website.com/about/ will work without any redirection because it will automatically search for an index.html inside its directory. But you need this proxy if you have param urls—https://my-website.com/profile/guillaume will need to redirect the request to /profile/index.html with its own layout, because profile/guillaume/index.html doesn’t exist and will trigger a 404 error.

A flowchart showing how a proxy makes a difference in a pre-rendering solution, as described in the previous paragraph

In short, there are three basic views at play with the rendering strategies described above: A loading screen, a skeleton, and the full page once it’s finally rendered.

Comparing a loading screen, a skeleton, and a fully-rendered page

Depending on the strategy, sometimes we use all three of these views, and sometimes we jump straight to a fully-rendered page. Only in one use case are we forced to use a different approach:

MethodLanding (e.g. /)Static (e.g. /about)Fixed Dynamic (e.g. /news)Parameterized Dynamic (e.g. /users/:user-id)
Client-renderedLoading → FullLoading → FullLoading → Skeleton → FullLoading → Skeleton → Full
Pre-renderedFullFullSkeleton → FullHTTP 404 (page not found)
Pre-rendered With ProxyFullFullSkeleton → FullSkeleton → Full
SSRFullFullFullFull

Client-only Rendering is Often Not Enough

Client-rendered applications are something we should avoid now because we can do better for the user. And doing better, in this case, is as easy as the pre-rendering solution. It’s definitely an improvement over client-only rendering and easier to implement than a fully server-side-rendered application.

An SSR/universal application can be really powerful if you have a large application with a lot of different pages. It allows your content to be focused and relevant when talking to a social crawler. This is also true for search engine robots, which now take your site’s performance into account when ranking it.

Stay tuned for a follow-up tutorial, where I will walk through the transformation of an SPA into pre-rendered and SSR versions, and compare their performance.

Micro-frontends: The path to a scalable future — part 1
26
Mar
2021

Micro-frontends: The path to a scalable future — part 1

Introduction

We have all heard the term microservices and perhaps have worked with them in the backend world. Before microservices, there were monolith applications. Back then, team and application growth was leading monolithic applications into a non-scalable dead-end. The codebase was growing, and technologies were getting older. All of this has made migrations and upgrades incredibly painful and frustrating.

An increase in project growth meant that multiple teams would unwittingly create further bottlenecks and inter-team dependencies. Deployments and releases were being micro-managed, but the growing amount of unmanaged technical debt would cause problems further down the line.

The same problems were present in the front-end world too. Again, you can structure the fanciest framework/library at that time and build your application with the best intentions. However, several years later, your tiny little application will inevitably become huge and force you to increase the size of your team. Eventually, this work will be split between multiple teams that will find themselves spending hours defining a release management schedule.

It’s around this time that you will hear about another cool library trending in the industry. Exploring the complexities of how to migrate to a new library while maintaining the old one typically leads to paranoia and eventually makes you give up on the idea because it’s just too much hassle. But you are also faced with a shrinking number of engineers who are willing to work with deprecated technologies. The chosen path has led you to a non-scalable dead-end.

A path to scalability

Try to imagine what the outcome would have looked like would we bravely decide to choose a different path. First, we should know that everything comes with a price, and with great power comes great responsibility. That power is the implementation of micro-frontends, which is

an architectural pattern to develop and maintain web application as a composition of small front-end applications

With back-end microservices, we had completely separate services with their separate DB and API. Microservices can be divided by business logic. For example, in an e-commerce app, we can have different microservices for a product catalog, user’s shopping cart, orders, etc. Without micro-frontend applications, it would have been a monolithic front-end application communicating with different back-end microservices (Fig. 1).

Figure 1. Application with monolith front-end and back-end microservices.

By splitting up the front-end into micro-frontends, we can align front-end and back-end architecture approaches and have a more robust and elegant full-stack architecture (Fig. 2).

Figure 2. End-to-end front-end and back-end microservices.
Figure 2. End-to-end front-end and back-end microservices.

With this approach, we can reach several significant benefits:

  • Team and technology autonomy: each team would have its own mission on the product and could select its own technology stack;
  • Small, maintainable, and de-coupled codebases: each team’s codebase would remain small and isolated from others’ codebases;
  • Independent release management: each team would be able to independently release its own part of the application saving a lot of time on inter-team communication and release schedule;
  • Painless upgrades and migrations: having small codebases and being free of inter-team dependency would lead to painless and independent upgrades of application technologies as well as migrations from the old one to a new one;

With the mentioned benefits, there are also challenges attached to implementing the micro-frontends architecture. There will still remain some issues and concerns that will be shared between teams such as web performance, a common design system, etc.

Even though the idea behind micro-services and micro-frontends is very similar, the implementation challenges are slightly different, and we will discuss them in the next section.

Micro-frontends integration concepts

There are three main concepts and problems to be taken into consideration when implementing the micro-frontend architecture.

Routing and page transition

This is one of the most important concepts. Regardless of how many micro-frontends are implemented in our application, it is crucial to handle transitions between micro-frontends (Fig. 3) properly. That said, users must never notice a switch from one micro-frontend application to another when navigating in the application.

Figure 3. The transition from one micro-frontend to another.

Page transition, which is also known as routing, can be either server or client-side. Let’s briefly go over each of them.

  • Server routing: on each transition (usually triggered by a click), there is a request to a server to obtain and serve the whole HTML document to the user. This method eventually leads to a full page reload, which is also known as a hard transition. It has several pros and cons and was a traditional way to handle transitions before the client-side routing evolution.
  • Client routing: on each transition, only some part of the application is being reloaded and operated by JavaScript in the browser on the client-side. Compared to server routing, there is no full page reload and is also known as soft transition. It brings a much better user experience and is implemented in almost all modern web applications.

There is an excellent article that covers server and client routing in more detail. With micro-frontends, the problem has various solutions starting from simple link transitions (the way routing was being handled in the 90’s and early 2000’s) into multi-layered client-side routings(a very modern way of having one top-level routing for the whole application, and several low-level routings per each micro-frontend). They differ by their implementation complexity, so the selection should be made based on the needs of our application.

Composition

It might be necessary to have a page owned by one team, but it will include fragments from other teams. This means we should consider the composition of multiple micro-frontends within each other (Fig. 4). For example, we could have an e-commerce web application and a product detail page owned by one team. We should also show the shopping cart content, which is owned by another team.

Figure 4. Composition of multiple micro-frontends.

Composition is a rendering concern which, similarly to routing, can be client and/or server-side:

  • Server-side rendering (SSR): with SSR or Universal rendering, our page document is being constructed and rendered on the server and served to the user. It is beneficial on the initial page load of an application as it is much more performant than rendering it on the client-side.
  • Client-side rendering (CSR): With CSR, our application page content is being constructed, rendered, and loaded on the client-side, operated by JavaScript. This type of rendering is used with all modern libraries and frameworks.

There are many cases when having just SSR or CSR will not be enough to meet our app needs (for example, when we need good SEO, a smooth user experience, and a fast content load). In these cases, the application uses both server and client-side rendering.

In most cases, the most relevant and essential content is being rendered server-side. It is being served to the user first, and afterward, the rest and more dynamic content is being handed over to JavaScript, which renders it on the client-side. There is much more to say about CSR, SSR but it is out of our topic for now. I highly recommend reading this article to learn more about these concepts and differences.

Tip: Again, most of the applications use both client and server-side rendering. But there is an easy life-hack that can help you check which parts of an application are rendered on the server-side and which parts on the client-side. Simply go to your browser settings and turn off JavaScript. Then open your desired website, and you will see the content which is loaded on the server-side. Then by turning on JS again, reloading the page will allow JS to render the other parts handed over to the client-side.

As with routing, there are also many different techniques both server-side(SSI, Zalando, Podium, etc.) and client-side(iframe, Ajax, Web Components, etc.) to handle micro-frontends composition. Which of these to go with depends on our application requirements.

Communication

The last concept of micro-frontend integration is the communication between our micro-frontends (Fig. 5).

Figure 5. Communication between micro-frontends.

Remember that we can have multiple micro-frontends on one page, and interaction with one can eventually lead to others’ changes. For example, let’s imagine we are on the product detail page of an e-commerce application, and we want to add a product to our shopping cart.

On the header (which is another micro-frontend fragment application), there is an icon of our cart, which also has an indicator — the number of added items. Adding the product on the details page should also update the number of items on our header’s shopping cart icon. There can be multiple communication scenarios like parent to fragment, fragment to parent, as well as fragment to fragment, and each communication is being handled differently.

  • Parent to fragment: This case is similar to handling communication between the parent component to the child components. It can be done by passing the data to children via props/attributes. So if our fragment/child is implemented with web components technology, it should be getting some attributes when they are being loaded, and data change in parent micro-frontend can trigger changed data passing to the fragment/child micro-frontends;
  • Fragment to parent: In this case, we can use the browser’s native CustomEvents API. So, on the parent side, there can be a subscribed event listener, while inside a fragment, we emit/publish an event on data change. The event emitter will bubble up the data to the listener on the parent side, which will then be handled on the parent micro-frontend.
  • Fragment to fragment: In this case, we can use the combination of the previous two techniques by emitting events to the parent fragment, which will listen to the changes and pass them to another fragment via props/attributes.

Another way of micro-frontends communication is Broadcast Channel API, which is an implementation of the Publisher/Subscriber design pattern just like the Custom Events API.

What’s next?

So, we have learned how beneficial micro-frontends can be and what problems they can solve. We also learned what challenges and problems we might face when implementing the micro-frontend architecture. We saw that based on the described three concepts, some various technologies and patterns could be used to achieve a full micro-frontend integration.

This should be enough to understand the main concepts and the big picture of micro-frontend architecture. This article is the first part of the series of 3 articles. The next article will be focused on high-level micro-frontend architecture types starting from the simplest and ending with the most complex one. We will also learn the concepts that will help us go with the architecture type that best fits our needs. So, stay tuned.