paint-brush
Breaking the Monolith: A Comprehensive Guide to Code Splitting Techniquesby@aleksandrguzenko
8,160 reads
8,160 reads

Breaking the Monolith: A Comprehensive Guide to Code Splitting Techniques

by Aleksandr GuzenkoApril 24th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Micro-frontends (MFs) can be used to organize development teams for large projects. MFs are more of an **organizational decision** on how to manage development complexity in a large project. MFs will not help you speed up the frontend, some implementations, on the contrary, will even slow it down.
featured image - Breaking the Monolith: A Comprehensive Guide to Code Splitting Techniques
Aleksandr Guzenko HackerNoon profile picture


Over the past few years, the front-end community has been actively discussing and using the term “micro-frontend” (hereinafter referred to as MF). Different companies share their approaches to organizing such an architectural solution, but so far there is little description of the problems that MFs are designed to solve on the Web, the criteria for their applicability, and the limitations in use.


In this article, I tried to compare different ways of organizing MFs, as well as to form recommendations on where to use which approach.


The article can be useful both for analysts and development teams when designing the architecture of a project and laying out processes, as well as for product owners since the introduction of MFs can provide more manageable development.

Microfront approach: what is it and why is it needed?

Before we move on to the definition of MF, let's look at a few problems that can be encountered in projects:


  1. You have a big project. The size of a project is usually subjective and can be empirically determined by the amount of functionality and the number of developers. If you have enough work to puzzle 1-2 front-enders and at the same time they will not “push their elbows” - this is a small project, 3-6 is medium, and more than 6-8 is already a big one.


  2. You have a big team. Again, empirically, this is more than 10 front-enders, the rest of the participants do not count. As a rule, a team of this size can already be divided into sub-teams that take on specific functionality for support, and acquire their own analysts, backenders, and QA.


  3. You have great functionality. A single developer can only maintain a piece of code for his subteam. Refining the rest of the code can be expensive due to ignorance of the subject area or the complexity of implementing third-party logic.


Problems can be further exacerbated:

  • the desire to change the stack;

  • a company requirement to support a family of related projects.


How can you organize relationships between so many people? How can you build processes on a project of this scale? How can you properly delineate areas of responsibility? The more problems you have collected, the more it is worth considering the introduction of a micro-frontal approach. Since this is a natural continuation of the evolutionary trend in development towards the decomposition of the code and the project team.





Thus, the MF approach is the division of a monolithic front into separate codebases stored in separate repositories, to which separate subteams have access. At the same time, they can/should have their own demo stands, tests, and release cycles. Accordingly, the microfront is a detachable piece of the interface. It is not necessary to divide by page, the functionality can be end-to-end (for example, corporate ui-kit).


Separately, it is worth highlighting that MFs are more of an organizational decision on how to manage development complexity in a large project. MFs will not help you speed up the frontend, some implementations, on the contrary, will even slow it down. But this approach will speed up the development itself due to the allocation of areas of responsibility and isolated testing.





Microfrontends vs Lazy Loading

Conversely, it is worth mentioning Lazy loading in comparison with MFs. They solve different problems, but sometimes people think that it's all about one thing, because in both cases we "split" the application.


Lazy loading solves the performance problem: how not to force the user to load the entire front-end bundle, how not to wait longer than necessary, and how to launch the front on the client faster and start interacting with it sooner.


MFs do not solve the performance problem, and sometimes even exacerbate it. But they help to organize the development in a way that is more comfortable for a particular subteam, minimizing the above problems.

Buildtime vs Runtime

Now let's talk about the approach of combining MFs into a single application. Whatever you choose, it should look like a single application to the user. You can merge both at the assembly stage and dynamically - during code execution on the user's side.


Thus, all ways of organizing MFs can be attributed to build time or runtime. Each has its pros and cons.


Buildtime

Runtime

Type checking

+

-

Versioning

+

no sense

Independent deploy

-

+


Type checking plays an important role in modern development. When it is run by separate independent sub-teams, it becomes a necessity. How to ensure the consistency of MFs, that they accurately use and pass data in the right format, etc.


By merging microfronts at build time, you are not deprived of the opportunity to check types. In the case of a runtime union, you will have to write integration tests so that the front does not suddenly “explode” on the production.


Versioning and independent deployment are very contradictory:


  • Versioning means that you can take any version of the other team's MF. This is especially true when you need to carry out additional work to upgrade the dependencies of the MF-a from others. Each team chooses a better time to upgrade.


  • Independent deployment gives more autonomy and independence to teams. It is important to always use the latest versions of MFs. This requires backward compatibility.


Versioning can also be implemented with runtime merging, but this is not practical. It makes sense to contact runtime only for the sake of independent deployment, and the latter cannot exist together with versioning.


Next, we will see examples of specific implementations of each approach to combining MFs.

Approaches to organizing microfrontends

Iframe

The oldest way to organize MFs. iframe is a special tag for passing the address of a resource that will be displayed on the main site. The result is a site within a site.




Of the advantages, it is worth noting the ease of implementation, complete isolation of logic and styles, the ability to do an independent deployment, and the absence of binding to frameworks.


These benefits come at a cost in performance, as each insertion of an iframe results in a load of resources. You can't avoid this: attempts to debug and reattach a DOM node won't save previously loaded resources, you'll have to redownload them. You can minimize performance degradation by configuring caching.


To do this, you need to configure time-based cache invalidation for immutable resources. Fortunately, all modern cli out of the box for the collected js and css files attach a small hash to the name. The disadvantages of this method include the inability of search robots to render iframes for subsequent indexing.


Pros

Cons

Easy implementing

Performance

Logic and styles isolation

SEO

Independent deploy


Framework agnostic



WebComponents

The front-end community has been waiting for the creation of native components for a long time, but in the end, they never gained the mass popularity that many expected. The three most popular front-end frameworks (React, Vue, Angular) still create components in their own way.


Despite the ability to create MFs on web components, I have not seen this in practice. And this is no accident, there are a number of blockers:


  • Either libs Lit or Stencil is not popular enough and not common enough. In addition, there are not enough specialists on the market who know how to work with them or who are ready to learn.


  • Angular elements or vue-custom-element remain exotic. In a native environment, there is not much point in using them. If you already split the application, then into ordinary npm packages, so that later you can connect the components as you like. Using web components with other frameworks is unreasonable because along with the generated components, you need to connect a mini-version of the framework on which they were written.


  • It can be costly to move complex pieces of functionality into web components. Since you will need to configure your component's communication with the rest of the application, it may not be justified to take out an entire page in a separate custom component.


  • Search robots cannot create a web component, and this will affect SEO optimization.

Pros

Cons

Suitable for cross-cutting functionality

Difficult to implement

Compatible with any framework

SEO

Logic and styles isolation



NPM

Developing with npm packages has many benefits. Developers simply import the components and files they need from the lib. At the same time, typing is preserved in the project, there is versioning. The build is optimal: tree-shaking works (removing unused code during build), and developers can easily set up lazy loading.



However, this method also has its drawbacks. In this case, you will be forced to maintain the unity of the stack, as well as maintain versions of your MFs and their transitive dependencies. On the other hand, this can be a plus: by publishing a new version of the package, other teams can take on the task of raising the version of their dependencies at a convenient time for them if they need to introduce additional functionality or, conversely, remove something.


Pros

Cons

Performance

Does not have independent deploy

SEO

Single stack

Type checking


Versioning



git submodules (or another way to make monorepos like Lerna)

As an alternative to MF on npm packages, consider git submodules. In fact, these are repositories within a repository, within which there can also be repositories. You can set different branches in submodules. For example, feature modules can have a dummy branch with nothing in it. This is necessary so that the assembly goes faster and other modules do not create side effects. Dummy branches can be very handy for local development and testing.







In terms of its advantages and disadvantages, this approach is very close to npm packages. We also just import something, but from a neighboring folder, and not from a package, and use it.


Let's take a look at the differences between the two methods:


  • NPM packages are, in fact, a finite, moderately isolated micro product with its own releases and versioning. Everything is focused on creating reusable functionality. But the application can be complex/convoluted and stinky, so it can be quite costly to package a large monolith. This is where it would be reasonable to consider submodules because they allow you to cut the repository very roughly when we move the folder to a separate repository without any additional preparation.


  • NPM packages can be nested recursively. Submodules too, but at the assembly level they can start to duplicate functionality if one of the submodules is included several times in different folders as a separate submodule. In this case, it is worth using a flatter module structure.


  • If you need to quickly roll out a feature in all packages at once, cross-package development can be extremely inconvenient. While everything stays the same on submodules, you can make edits that affect other submodules. In this case, it is easy to debug. But in the end, you won’t merge the changes themselves - at the merge request level, a third-party team whose module you touched may require you to bring the code in line with their rules.


npm

git submodules

Reusability

Rough cutting of functionality

Arbitrarily nested dependencies

Flat structure

Cross-platform development

Development with any modules count


single-spa

single-spa is essentially a framework that combines other frameworks. Incredibly powerful technology, which hides a huge number of nuances, is a topic for a separate article.

The scheme is similar to iframe, but the loading of the MF is now done through native import + importmap or through Systemjs if polyfills are needed.





Unlike all methods of organizing MFs, this one is highly tailored for combining different frameworks under itself. But it is worth cautioning against using technology for the sake of technology itself. If it is possible to get by with one stack, you need to use it. Development can be forever burdened with maintaining a technically complex project and fixing any bugs from the side effects of different applications. The user may feel discomfort, because. the amount of code for downloading to the client will be increased (the cores of different frameworks for different pieces of functionality + the core of the single-spa itself and its plugins).

Pros

Cons

Independent deploy

Large documentation, which still does not cover all cases

Framework agnostic

Difficulties with SEO

Powerful CLI



Webpack 5 Module Federation

A webpack 5 plugin that was developed specifically for creating MFs. Promising technology: a small build plugin for more correct bundling and dynamic imports at runtime.


The scheme almost one-on-one repeats single-spa, but now dynamic imports are used to load MFs



Pros

Cons

Independent deploy

low level

Easy realization


Compatible with SSR




How to choose what to use in your case?

Let's take a look at what can be applied and for what:


  • iframe - a single insert for a combination of incongruous

  • web components - when you need a small end-to-end functionality without being tied to a framework, like a corporate ui-kit

  • npm packages - if there is reusability between projects and/or you need type checking in build time

  • git submodules - when you need to roughly chop a project and distribute areas of responsibility

  • single-spa - when there is a strong need to combine multiple frameworks indefinitely, preferably without SSR

  • module-federation - all other scenarios for the use of MFs, subject to the unity of the stack.


Each approach is good in its own way and everything should have its place. Before switching to MFs, we advise you to think about whether you really need it. Whichever approach is chosen, it will inevitably complicate something at the level of development, CI / CD or performance. If there are opportunities to stay on a single stack and a monolithic application, gladly accept this opportunity.


And, of course, do not forget about the users. Ultimately, they download all connected frameworks and endure possible bugs from incorrect integration of MFs in different pieces of functionality. Businesses, in turn, will have to pay for the implementation and support of all this.