DEV Community

Cover image for Micro Frontends: After one year with Single-SPA
Samim Pezeshki
Samim Pezeshki

Posted on

Micro Frontends: After one year with Single-SPA

Why did we choose micro frontend architecture?

We have a codebase that was originally written using AngularJS. After some years and when finally the news about AngularJS end of life came around, we started migrating to Angular (actually hybrid Angular/AngularJS). Finally, two years ago we successfully migrated to Angular (You can read the details in another post) having high hopes that by migrating to it we can leverage a new ecosystem. But after some months it became evident that Angular and AngularJS are so different that we need to rewrite everything, which is not very pleasant. Also, the React ecosystem and talent pool seemed much more vibrant so investing again in Angular for new upcoming features seemed like a non-optimal long-term solution. Over the years there were more experienced React developers in our teams so developing features in React would be much faster than having them in Angular.

So we were looking for options to be able to keep our current Angular app while being able to add new upcoming features and sections using React or other frameworks. After some search, we found out that micro frontends were the solution we were looking for! Using Single-SPA one can have multiple frameworks, Angular and React, running side by side. Single-SPA is composed of so-called apps each being a SystemJS or ES module. Each app can use a different framework and technology and it only needs to mount itself somewhere on the page. Apps are mounted and unmounted based on the page route. All of this happens client-side. As a side note, I was thinking if we had known about micro frontends, maybe we would never have migrated to hybrid Angular and would have chosen Single-SPA from the beginning.

Micro frontends are created for various purposes. Mainly it is discussed as a solution for keeping release cycles, deployments, and decisions in each team independent of others, like microservices but for frontend. In our case, we settled on micro frontends to be able to extend the lifetime of a legacy codebase by being able to take advantage of newer frameworks alongside old ones.

We also assessed some other micro frontend frameworks and solutions, even using iframes and server-side routing, but finally, we decided to go with SignleSPA as it is less opinionated, simple and the best fit for our current codebase. The website is fully static (Angular/AngularJS) and is served from a CDN, so using server-side routing was out of options.

Benefits

The main benefit was improving the developer experience. Each Single-SPA app is developed separately, so when a developer starts to work on a React app (Single-SPA app) he/she does not need to install all the dependencies for other apps, like Angular, or to know how other apps are configured. Also because each app is small the development cycle of local builds, hot-reloads, and tests are much shorter in time. Developers can build features (Single-SPA apps) truly independently and separately. So now we could use all the experiences of our React developers in our legacy website.

Each app in single-SPA is bundled separately. Using different apps for different features results in multiple small chunks, instead of a big fat bundle. Splitting the bundle can also be done by configuring Webpack without Single-SPA, but here we got it for free.

Apart from smaller chunks and bundles we got lazy loading too. Some features are not used frequently, so their bundle can be loaded separately in the background after the initial load.

As new feature apps are developed using React, even after migration to a whole new framework like NextJS in the future, those parts can be re-used without the need to rewrite everything from scratch.

Issues

One issue I had was that I could not generate source maps for Angular when it was built as a SystemJS module. I did not dig deep into the issue as it did not have a great impact on the project. But it is was nice to have source maps.

Another issue was the integration between the apps. We used local storage, global events, and shared modules for this and they all worked pretty well. But deciding on the best option was sometimes challenging.

Also as the whole concept is new, it took some time for the new developers to learn how to get on track, although this was negligible and even sometimes exciting to learn about new trends.

Code structure and deployment pipelines

All Single-SPA apps are put into a single repository. Each app has its own package.json file and is developed and built separately. There is also the root app which contains the main router responsible for mounting and unmounting other apps.

├── apps
│   ├── root
│   │   ├── node_modules
│   │   ├── package.json
│   │   └── src
│   │       └── index.html
│   ├── feature-one (Angular)
│   │   ├── node_modules
│   │   └── package.json
│   └── feature-two (React)
│       ├── node_modules
│       └── package.json
└── scripts
    ├── build.sh
    ├── deploy.sh
    └── start.sh
Enter fullscreen mode Exit fullscreen mode

During deployment, there is a shell script that installs and builds each app and assembles them by copying the built files into a final build directory. Then it uses AWS Cloudformation to create a static website on S3, CloudFront, and Route53.

export ROOT_PATH=$PWD
export VERSION=4.0-$(git log -1 --pretty="%h")${BUILD_NUMBER}-$(date --iso)

for d in ./apps/*; do
  if [ -d "$d" ]; then
    echo " * Installing dependencies for $d"
    echo
    cd $d
    npm install
    npm run build
    mv dist $ROOT_PATH/dist/$d
    cd -
  fi
done
Enter fullscreen mode Exit fullscreen mode

As a single deployment pipeline and repository is used for all the apps, we are not gaining from one of the main benefits of using micro frontends architecture which is independent release cycles for each app. But by putting everything in a single repository we could achieve what we were looking for without dealing with the complexity of managing multiple repositories and deciding on how to update import maps (solutions like import-map-deployer).

Development Experience

There are two ways to start developing. One is using the single-spa-inspector browser extension. This way the developer opens the fully-deployed live website (not localhost:3000 or any local address) and overrides the import maps to make the live website connect to the Single-SPA app running locally. This way the developer only runs the one feature app he/she is working on while running it inside the live deployed website. It frees the developer from running the whole website locally and even has the side benefit of seeing and developing the feature app in the context of the deployed website connected to the live database. This way of development was personally very unique and new to me, it was amazing.

Another approach is to start all Single-SPA apps locally. This approach is sometimes needed for debugging the integration between the apps. The below script is used to run all apps:

SCRIPT_ENV="${1:-dev}"
PORT=3000

echo "⚜    Starting ${SCRIPT_ENV}..."
echo
echo ⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
echo "📡   Listening on https://localhost:${PORT}"
echo ⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽⎽
echo

echo "⚠    Ignore if the below single-spa apps report about their ports! root app is served on port ${PORT}."
echo

npx concurrently --names "ROOT,FEATURE1,FEATURE2" \
  -c "#E0E0E0,#26C6DA,#FFA726" \
  "cd apps/root && env PORT=${PORT} npm run start:${SCRIPT_ENV}" \
  "cd apps/feature-one && env PORT=$(expr ${PORT} + 1) npm run start:${SCRIPT_ENV}" \
  "cd apps/feature-two && env PORT=$(expr ${PORT} + 2) npm run start:${SCRIPT_ENV}"
Enter fullscreen mode Exit fullscreen mode

Road ahead

Adopting micro frontend architecture (Single-SPA) enabled us to further keep our legacy website while utilizing more trendy technologies to deliver new features. Otherwise, we had to rewrite the whole website or stick to what we had. Now that new features are delivered on time and we are on schedule plans can be made to rewrite the whole website without a rush.

With new trends, frameworks, and ideas popping up in the web development space every day, like server-side rendering, statically generated dynamic content, edge serverless workers, etc., I am not sure if we would again choose Single-SPA for a project creating from scratch. But for our use case, the micro frontend architecture served us well. If you have any framework or architecture in mind to suggest for our next project, please share, I would appreciate it.

Discussion (8)

Collapse
thomasburleson profile image
Thomas Burleson • Edited on

I appreciate your blog and efforts.
This article, however, is deeply flawed and misleads the community.

Using Nx Workspaces and monorepos, devs can successfully build apps, libs, and mfes... With independent releases and independent teams.

Using MFEs are fraught with runtime problems: sharing state, server thrashing, perform issues, logging, and VERSIONing.

Collapse
psamim profile image
Samim Pezeshki Author

Thanks for reading.

I just learnt about Nx workspaces, seems great Sure monorepoes can be used to achieve high level of independency between packages. Although we were not looking for independent releases or independent teams, we chose MFEs to let apps with different frameworks coexist, we needed a client-side router, and this solution as you said has its own mentioned advantages and disadvantages. I would appreciate if you could elaborate more if I got it wrong.

Collapse
thomasburleson profile image
Thomas Burleson

SPAs enable teams to use components deliver engaging UX, business features, and associated business logic as a self-contained solution for modern web apps. Even with client-side view routing and on-demand loading these SPAs are build-time solutions. These solutions often are deployed as full-page immersive experiences.

1) MFEs

Within the last couple of years ideas have emerged to deploy these SPAs as widgets or micro-experiences. The approach is to then allow a host page to compose multiple SPAs within custom layouts on a full-page.

I think using micro-experiences published as independent SPAs is a flawed solution. Yes the Single-SPA is rather creative for providing client-side routing AND even limited container layout.

That solution does not address however the issues of

  • how do we handle version-incompatibility issues
    • For example, what if two different SPAs are loaded using the same framework; but each requires DIFFERENT versions of the framework or libraries?
  • how do we constrain 1 MFE from routing the entire full-page experience
  • how do we choreograph animations and transitions across MFEs
  • how do we ensure consistent theme?
  • how do we prevent CSS bleeds or overrides across MFEs within the same browser window layout?
  • how do the SPAs communicate easily with each other? Shared data or global services?
  • how is authentication supported?

These are hard questions that must be addressed when thinking that MFEs solve the problems with multiple teams working in parallel or with different technologies.

2) Component-based SPA Development

Another approach is something that @bitdev_ (bit.dev) is working on: allowing MFEs to be published as components. And then allowing those MFE components to be composed and development time and build/deployed as a single SPA.

This is another interesting approach that has IMO many of the challenges enumerated above.

3) Component development

If developers use an Nx workspace, then React, Vue, Angular, and NextJS solutions can be created using shared business logic and services (implemented in TypeScript). While the views are not shareable, the business and data services are shared. Architected properly this is actually 70%-80% of an apps core code IMO.

Thread Thread
psamim profile image
Samim Pezeshki Author

Thanks for the detailed explanation! I agree, I faced the problems you listed and as you said Single-SPA does not provide a set of solutions for them. So I had to find and gather solutions.

I used shared modules for authentication, communication between apps and business logic, same as you proposed, sharing data services. Having shared modules is the best whether you use components or even MFEs.

As the website was developed by one team, it was easy to have no style overrides and styles were encapsulated by Angular's shadow DOM and React's CSS modules. Also for each app, Webpack can be configured to use the externally available SystemJS (or ES) module or bundle the module. So if two apps are dependent on two different versions of a module, there is no issue apart from the bundle being fat. They each bundle their own version. If they use the same version, they can both use the externally shared module. Anyway it is recommended to use the same version.

I do agree that agreeing on the above solutions and communicating the decisions and conventions may be hard for larger teams, but it was not a problem for us.

About the third solution, component development, yes I think I could go that way and it may be cleaner in many ways, especially using nice tools like Nx. Things could be designed cleaner if we were doing it from scratch. But I am not sure how the development setup would be like using component development (3rd). We were able to only run a single app locally, and override the import-maps using the Single-SPA extension to make the live website to connect to the local app using all the React dev features like hot reloads, which our original AngularJS app did not have. New developers did not need to even build or run the whole website or know how it works. Using components I cannot imagine how to do that, I need to dig deeper in Nx and monorepos. to find out. Also using shared logic and data services (70%-80% of the code) can be achieved using MFEs too, by sharing modules.

Collapse
punisher49 profile image
punisher49 • Edited on

I am not sure if putting everything into one repo is a good idea.

Collapse
psamim profile image
Samim Pezeshki Author • Edited on

@punisher49 I agree. This way we are missing having independent release cycles, which is the one of the main benefits of using micro frontends. When there are separate teams working on different features, putting each mico frontend into a separate repository gives them the power of having independent release cycles, internal management and technical and non-technical team decisions.

But in our case, all the codebase was maintained and developed by one team. The main objective of using micro frontends was to let different frameworks co-exist. So putting the apps into separate repositories would add the complexity of managing multiple repositories, PRs, different versions for each app in our error and issue tracking service, and also implementing a solution for updating import maps (like using import-map-deployer). So we accepted the trade-off.

But I might be missing something here. I would appreciate it if you could elaborate and explain what other possible benefit we might be missing this way.

Collapse
altjeno profile image
altjeno

Hi,

Thanks for the article! Could I ask why you wrote at the end, that maybe you wouldn't choose single-spa for a new project? Do you think that single-spa wont be a long-term solution?

Collapse
psamim profile image
Samim Pezeshki Author

@altjeno
Hi! Thanks for taking your time!

I really like the Single-SPA development experience. But I think there are requirements and use cases which leads to choosing the micro frontend architecture. For example, you /can/ implement SSR and Single-SPA, but then you need to decide on details. On the other hand, if you use something like NextJS there is already a nice ready-made interface to add SSR and SSG to your app. But by using NextJS you are tied to using only one framework, React. There are also more opinionated ideas and frameworks which are not compatible with Single-SPA which you may want to pick.

So there are trade-offs. If the project is small enough to be planned, developed and maintained by one team and the team is confident enough in choosing a framework, I would choose a framework with less complexity. But in a case where you need the features that Single-SPA provides, i.e. having fully independent micro-frontends maintained by separate teams with separate codebases, release cycles and frameworks, Single-SPA is ideal.