Chapter 1. The Frontend Landscape

I remember a time when web applications were called rich internet applications (RIAs) to differentiate them from traditional, more static corporate websites. Today, we can find many RIAs, or web applications, across the World Wide Web. There is a proliferation of online services that allow us to print business cards on demand, watch our favorite movies or live events, order a pepperoni pizza, manage our bank accounts from our comfortable sofas, and do many, many other things that make our lives easier.

As CTOs, architects, tech leads, or developers, when we start a greenfield project, we can create a single-page application or an isomorphic one, whose code can run in both the server and the client, or even work with a bunch of static pages that run in our cloud or on-premises infrastructure. While we now have such a broad range of options, not all are fit for every job. To make the right decision for our projects, we need to understand the challenges we will face along the way.

Before we jump into the topic of this book, let’s analyze the current architectures available to us when we work on a frontend application.

Micro-Frontend Applications

Micro-frontends are an emerging architecture inspired by microservices architecture. The main idea behind it is to break down a monolithic codebase into smaller parts, allowing an organization to spread out the work among autonomous teams, whether collocated or distributed, without the need to slow down their delivery throughput.

However, designing an API and encapsulating the logic into a microservice is actually the easiest part. When we realize there is significantly more to take care of, we will understand the complexity of the microservices architecture that adds not only high flexibility and good encapsulation between domains but also an overall complexity around the observability, automation, and discoverability of a system.

For instance, after creating the business logic of a service, we need to understand how a client should access our API.  If it’s an internal microservice that should communicate with other microservices, we need to identify a security model. Then we need to deal with the traffic that consumes our microservice, implementing techniques for spike traffic like autoscaling or caching. We also need to understand how our microservice may fail. It may fail gracefully without affecting the consumers and just hiding the functionality on the user interface. Otherwise, we need to have resilience across multiple availability zones or regions.

Working with microservices simplifies the business logic, but we need to handle an intrinsic complexity at different levels like networking, persistence layer, communication protocols, security, and many others. This is also true for micro-frontends. If the business logic and the code complexity are reduced drastically, the overhead on automation, governance, observability, and communication have to be taken into consideration.

As with other architectures, micro-frontends might not be suitable for all projects; existing architectures such as server-side rendering or Jamstack are still valid options. Nevertheless, micro-frontends can provide a new way to structure our frontend applications at scale, solving some key scalability challenges companies have encountered in the past from both a technical and organizational perspective.

Too often I have seen great architectures on paper that didn’t translate well into the real world because the creator didn’t take into account the environment and its context (company’s structure, culture, developers’ skills, timeline, etc.) where the project would have been built.

Melvin Conway’s law put it best: “Any organization that designs a system (defined more broadly here than just information systems) will inevitably produce a design whose structure is a copy of the organization’s communication structure.”1 Conway’s law could be mitigated with the inverse Conway maneuver, which recommends that teams and organizations be structured according to our desired architecture and not vice versa. I truly believe that mastering different architectures and investing time in understanding how many systems work allow us to mitigate the impact of Conway’s law, because it gives us enough tools in our belt to solve both technical and organizational challenges.

Micro-frontends, combined with microservices and a strong engineering culture where everyone is responsible for their own domain, may help achieve organizational agility and better time to market. This architecture can be used in combination with other backend architecture, such as a monolith backend or service-oriented architecture (SOA). However, micro-frontends are suited well when we can have a microservices architecture, allowing us to define slices of an application that are evolving together.

Single-Page Applications

Single-page applications (SPAs) consist of a single or a few JavaScript files that encapsulate the entire frontend application, usually downloaded up front. When the web servers or the content delivery network (CDN) serves the HTML index page, the SPA loads the JavaScript, CSS, and any additional files needed for displaying any part of our application. Using SPAs has many benefits. For instance, the client downloads the application code just once, at the beginning of its life cycle, and the entire application logic is then available up front for the entire user’s session.

SPAs usually communicate with APIs by exchanging data with the persistent layer of the backend, also known as the server side. They also avoid multiple round trips to the server for loading additional application logic and render all the views instantaneously during the application life cycle. 

Both features enhance the user experience and simulate what we usually have when we interact with a native application for mobile devices or desktop, where we can jump from one part of our application to another without waiting too long.

In addition, an SPA fully manages the routing mechanism on the client side. What this means is, every time the application changes a view, it rewrites the URL in a meaningful way to allow users to share the page link or bookmark the URL for starting the navigation from a specific page. SPAs also allow us to decide how we are going to split the application logic between server and client. We can have a “fat client” and a “thin server,” where the client side mainly stores the logic and the server side is used as a persistence layer, or we can have a “thin client” and a “fat server,” where the logic is mainly delegated to the backend and the client doesn’t perform any smart logic but just reacts to the state provided by the APIs.

Over the past several decades, different schools of thought have prevailed on whether fat or thin clients are a better solution. Despite these arguments, however, both approaches have their pros and cons. The best choice always depends on the type of application we are creating. For example, I found it very valuable to have a thin client and a fat server when I was targeting cross-platform applications. It allowed me to design a feature once and have all the clients deployed on multiple targets react to the application state stored on the server.

When I had to create desktop applications in which storing some data offline was an essential feature, I often used a fat client and a thin server instead. Rather than managing the state logic in two places, I managed it in one place and used the server for data synchronization.

However, SPAs have some disadvantages for certain types of applications. The first load time may be longer than those of other architectures because we are downloading the entire application instead of only what the user needs to see. If the application isn’t well designed, the download time could become a killer for our applications, especially when they are loaded with an unstable or unreliable connection on mobile devices such as smartphones and tablets.

Nowadays, we can cache the content directly on the client in several ways to mitigate the problem. On top of most consolidated techniques like code splitting or lazy-loading of JavaScript bundles, a technique worth a mention is using progressive web apps. Progressive web apps provide a set of new capabilities based on service workers. A service worker is a script that your browser runs in the background, separate from a web page, for providing functionality such as offline experience or push notifications.

Thanks to service workers, we can now create our caching strategy for a web application, with native APIs available inside the browsers. This pattern is called offline first, or cache first, and it’s the most popular strategy for serving content to the user. If a resource is cached and available offline, return it first before trying to download it from the server. If it isn’t in the cache already, download it and cache it for future usage. It’s as simple as that but very powerful for enhancing the user experience in our web application, especially on mobile devices.

Another disadvantage relates to search engine optimization (SEO). When a crawler—a program that systematically browses the World Wide Web in order to create an index of data—is trying to understand how to navigate the application or website, it won’t have an easy job indexing all the contents served by an SPA unless we prepare alternative ways for fetching it.

Usually, when we want to provide better indexing for an SPA, we tend to create a custom experience strictly for the crawler. For instance, Netflix lowers its geofencing mechanism when the user-agent requesting its web application is identified as a crawler rather than serving content similar to what a user would watch based on the country specified in the URL. This is a very handy mechanism, considering that the crawler’s engine is often based in a single location from which it indexes a website all over the world.

Downloading all the application logic in one go can be a disadvantage as well because it can lead to potential memory leaks when the user is jumping from one view to another if the code is not well implemented and does not correctly dispose of the unused objects. This could be a serious problem in large applications, leading to several days or weeks of code refactoring and improvements in order to make the SPA code functional. It could be even worse if the device that loads the SPA doesn’t have great hardware, like a smart TV or a set-top box. Too often I have seen applications run smoothly on a MacBook Pro quad-core and then fail miserably when running on a low-end device.

An SPA’s last disadvantage is on the organizational side. When an SPA is a large application managed by distributed or colocated teams working on the same codebase, different areas of the same application could end up with a mix of approaches and decisions that could confuse team members. The communication that overhead teams use to coordinate between themselves is often a hidden cost of the application.

We often completely forget about calculating the inefficiency of our teams, not because they are not capable of developing an application but because the company structure or architecture doesn’t enable them to express it in the best way possible, slowing down the operations, creating external dependencies, and overall generating friction during the development of a new feature. Also, the developers may feel a lack of ownership since many key decisions may not come from them and since the codebase of a large SPA may be started months, if not years, before they join the company.

All of these situations are not presented in the form of an invoice at the end of the month, but they might impact the teams’ throughput since a complex codebase may slow down drastically the team’s potential for delivery. 

Isomorphic Applications

Isomorphic or universal applications are web applications where the code between server and client is shared and can run in both contexts. It is particularly beneficial for the time to interaction, A/B testing, and SEO. Thanks to the possibility of generating the page on the server side, we are in charge of optimizing our code for the key characteristics of our project.

These web applications share code between server and client, allowing the server, for instance, to render the page requested by the browser, retrieve the data to display from the database or from a microservice, aggregate it, and then prerender it with the template system used for generating the view in order to serve to the client a page that doesn’t need additional round trips for requesting data to display.

Because the page requested is prerendered on the server side and is partially or fully interpreted on the backend, the time to interaction is enhanced. This avoids a lot of round trips on the frontend, so we won’t need to load additional resources (vendors, application code, etc.), and the browser can interpret a static page with almost everything inside.

An SEO strategy can also be improved with isomorphic applications because the page is rendered server side without the need for additional server requests. When served, it provides the crawler an HTML page with all the information inside ready to be indexed immediately without additional round trips to the server.

Isomorphic applications share the code between contexts, but how much code is really shared? The answer depends on the context. For instance, we can use this technique in a hybrid approach, where we render part of the page on the server side to improve the time to interact and then lazy-load additional JavaScript files for the benefits of both the isomorphic application and the SPA. The files loaded within the HTML page served will add sophisticated behaviors to a static web page, transforming this page into an SPA.

With this approach, we can decide how much code is shared on the backend based on the project’s requirements. For example, we can render just the views, inlining the CSS and the bare minimum JavaScript code to have an HTML skeleton that the browser can load very quickly, or we can completely delegate the rendering and data integration onto the server, perhaps because we have more static pages than heavy interactivity on the client side. We can also have a mixed approach, where we divide the application into multiple SPAs, with the first view rendered on the server side and then some additional JavaScript downloaded for managing the application behaviors, models, and routing inside the SPA.

Routing is another interesting part of an isomorphic application because we can decide to manage it on the server side, only serving a static page any time the user interacts with a link on the client.

Or we can have a mixed approach. We can use the benefits of server-side rendering for the first view and then load an SPA, where the server will do a global routing that serves different SPAs, each with its own routing system for navigating between views. With this approach, we aren’t limited to template libraries; we can use virtual Document Object Model (DOM) implementations like React or Preact. Many other libraries and frameworks have started to offer server-side rendering out of the box, like Vue with Nuxt.js, Meteor, and Angular.

The last thing to mention about isomorphic applications is that we can integrate A/B testing platforms nicely without much effort. A/B testing is the act of running a simultaneous experiment between two or more variants of a page to see which one performs the best. In the past year or so, many A/B testing platforms had to catch up with the frontend technologies in not only supporting UI libraries like jQuery but also embracing virtual DOM libraries like React or Vue. Additionally, they had to make their platforms ready for hybrid mobile applications as well as native ones.

The strategy these companies adopted is to manage the experiments on the server side, leveraging the isomorphic characteristic of running on the server and client side. This is obviously a great advantage if you are working with an isomorphic application, because you can prerender on the server the specific experiment you want to serve to a specific user. Those solutions can also communicate with the clients via APIs with native mobile applications and SPAs for choosing the right experiment.

But isomorphic applications could suffer from scalability problems if a project is really successful and visited by millions of users. Because we are generating the HTML page prerendered on the server, we will need to create the right caching strategy to minimize the impact on the servers. In this case, if the responses are highly cacheable, CDNs like Akamai, Fastly, or Amazon CloudFront could definitely improve the scalability of our isomorphic applications by avoiding all the requests hitting origin servers. Organization-wise, an isomorphic application suffers similar problems as an SPA whose codebase is unique and maintained by one or multiple teams.

There are ways to mitigate the communication overhead if a team is working on a specific area of the application without any overlap with other teams. In this case, we can use architecture like backends for frontends (BFF) for decoupling the API implementation and allow each team to maintain their own layer of APIs specific to a target.

Static-Page Websites

Another option for your project is the static-page website, where every time the user clicks on a link, they are loading a new static page. This is fairly old school, but it’s still in use—with some twists. A static-page website is useful for quick websites that are not meant to be online for a long period, such as ones that advertise a specific product or service we want to highlight without using the corporate website or that are meant to be simple and easier to build and maintain by the end user.

In the last few years, this type of website has mutated into a single page that expands vertically instead of loading different pages. Some of these sites also lazy-load the content, waiting until the user scrolls to a specific position to load the content. The same technique is used with hyperlinks, where all the links are anchored inside the same page and the user is browsing quickly between bits of information available on the website. These kinds of projects are usually created by small teams or individual contributors. The investment on the technical side is fairly low, and it’s a good playground for developers to experiment with new technologies or new practices or to consolidate existing ones.

Jamstack

In recent years, a new frontend architecture called Jamstack (JavaScript, APIs, and Markup) emerged with great results.Jamstack is intended to be a modern architecture to help create fast and secure sites and dynamic apps with JavaScript/APIs and prerendered markup, served without web servers. In fact, the final output is a static artifact composed of HTML, CSS, and JavaScript, basically the holy trinity of frontend development. The artifact can be served directly by a CDN since the application doesn’t require any server-side technology to work. One of the simplest ways for serving a Jamstack application is using GitHub pages for hosting the final result. In this category, we can find popular solutions like Gatsby.js, Next.js, or Nuxt.js.

The key advantages of these architectures are better performance and cheaper infrastructure and maintenance since they can be served directly by a CDN; great scalability because they serve static files; higher security due to the decrease of attack surface; and easy integration with headless CMS.

Jamstack is a great companion for many websites we have to create, especially considering the frictionless developer experience. In fact, frontend developers can focus only on the frontend development and debugging, and this usually means a more focused approach on the final result.

Summary

Over the years, the frontend ecosystem has evolved to include different architectures for solving different problems. A piece has been missing, though: a solution that would allow for the scaling of projects with tens or hundreds of developers working on the same project. Micro-frontends are that missing piece.

Micro-frontends will never be the only architecture available for frontend projects. Yet they provide us with multiple options for creating frontend projects at scale. Our journey in learning micro-frontends starts with their principles, analyzing how these principles should be leveraged inside an architecture and how much they resemble microservices.

1 Melvin E. Conway, “How Do Committees Invent?” Thompson Publications, Inc., 1968. Mel Conway’s Home Page, accessed October 4, 2021, https://www.melconway.com/Home/Committees_Paper.html.

Get Building Micro-Frontends now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.