Skip to content

Commit

Permalink
perf(platform): reduce library bundle size in client application
Browse files Browse the repository at this point in the history
closes #174

BREAKING CHANGE: reducing bundle size in client application introduced a breaking change for host and client applications. The communication protocol between host and client has not changed, i.e., host and clients can be updated independently to the new version.

To enable tree-shaking of the SCION Microfrontend Platform, the platform was split into three separate entry points:
- `MicrofrontendPlatformHost` to configure and start the platform in the host
- `MicrofrontendPlatformClient` to connect to the platform from a microfrontend
- `MicrofrontendPlatform` to react to platform lifecycle events and stop the platform

The size of the library bundled in a client application could be reduced by more than 50%, from 120 KB to 40 KB.

To migrate the host application:
  - start the platform via `MicrofrontendPlatformHost.start` instead of `MicrofrontendPlatform.startHost`
  - monitor startup progress via `MicrofrontendPlatformHost.startupProgress$` instead of `MicrofrontendPlatform.startupProgress$`

To migrate the client application:
  - connect to the host via `MicrofrontendPlatformClient.connect` method instead of `MicrofrontendPlatform.connectToHost`
  - test if connected to the host via `MicrofrontendPlatformClient.isConnected` method instead of `MicrofrontendPlatform.isConnectedToHost`
  • Loading branch information
danielwiehl authored and Marcarrian committed Dec 21, 2022
1 parent 755422e commit fff9953
Show file tree
Hide file tree
Showing 90 changed files with 2,254 additions and 1,263 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,22 @@ jobs:
path: ./node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- run: npm run test:headless
analyze:
name: 'Analyzing'
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
- name: 'Restoring NPM modules from cache'
uses: actions/cache@v2
with:
path: ./node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- name: 'Analyzing ''@scion/microfrontend-platform'' bundle in client application'
run: npm run microfrontend-platform:analyze:assert
e2e:
name: 'E2E Testing'
needs: [build-platform, build-apps]
Expand Down Expand Up @@ -162,6 +178,7 @@ jobs:
- build-apps
- lint
- test
- analyze
- e2e
runs-on: ubuntu-latest
outputs:
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ The following is a summary of commands useful for development of `scion-microfro

- `npm run microfrontend-platform:test`\
Runs unit tests of the microfrontend-platform library.

- `npm run microfrontend-platform:analyze`\
Displays the content of the library if installed in a client app. Use to verify the library to be tree shaken correctly, i.e., that the host module is not included.

### Commands for running end-to-end tests

Expand Down
42 changes: 42 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,48 @@
}
}
},
"@scion/microfrontend-platform.client-application-bundle-analyzer": {
"projectType": "application",
"root": "projects/scion/microfrontend-platform.client-application-bundle-analyzer",
"sourceRoot": "projects/scion/microfrontend-platform.client-application-bundle-analyzer/src",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/microfrontend-platform-client-application-bundle-analyzer",
"index": "projects/scion/microfrontend-platform.client-application-bundle-analyzer/src/index.html",
"main": "projects/scion/microfrontend-platform.client-application-bundle-analyzer/src/main.ts",
"polyfills": "projects/scion/microfrontend-platform.client-application-bundle-analyzer/src/polyfills.ts",
"tsConfig": "projects/scion/microfrontend-platform.client-application-bundle-analyzer/tsconfig.app.json",
"outputHashing": "all",
"sourceMap": true,
"namedChunks": true,
"optimization": {
"styles": false,
"scripts": true,
"fonts": true
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/scion/microfrontend-platform.client-application-bundle-analyzer/src/test.ts",
"tsConfig": "projects/scion/microfrontend-platform.client-application-bundle-analyzer/tsconfig.spec.json",
"karmaConfig": "projects/scion/microfrontend-platform.client-application-bundle-analyzer/karma.conf.js"
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"projects/scion/microfrontend-platform.client-application-bundle-analyzer/**/*.ts"
"projects/scion/microfrontend-platform.client-application-bundle-analyzer/**/*.html"
]
}
}
}
},
"microfrontend-platform-testing-app": {
"projectType": "application",
"schematics": {
Expand Down
4 changes: 2 additions & 2 deletions apps/microfrontend-platform-devtools/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {Observable, Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {animate, style, transition, trigger} from '@angular/animations';
import {ShellService} from './shell.service';
import {MicrofrontendPlatform} from '@scion/microfrontend-platform';
import {MicrofrontendPlatformClient} from '@scion/microfrontend-platform';

@Component({
selector: 'devtools-root',
Expand All @@ -34,7 +34,7 @@ export class AppComponent implements OnDestroy {
public showPrimaryOutlet = true;
public showDetailsOutlet = false;
public menuOpen = false;
public readonly connnectedToHost = MicrofrontendPlatform.isConnectedToHost();
public readonly connnectedToHost = MicrofrontendPlatformClient.isConnected();

private _destroy$ = new Subject<void>();

Expand Down
4 changes: 2 additions & 2 deletions apps/microfrontend-platform-devtools/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {APP_INITIALIZER, inject, NgModule, NgZone} from '@angular/core';

import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {IntentClient, ManifestService, MessageClient, MicrofrontendPlatform, ObservableDecorator, OutletRouter} from '@scion/microfrontend-platform';
import {IntentClient, ManifestService, MessageClient, MicrofrontendPlatformClient, ObservableDecorator, OutletRouter} from '@scion/microfrontend-platform';
import {NgZoneObservableDecorator} from './ng-zone-observable-decorator';
import {AppDetailsComponent} from './app-details/app-details.component';
import {AppListComponent} from './app-list/app-list.component';
Expand Down Expand Up @@ -102,6 +102,6 @@ export function providePlatformInitializerFn(): () => Promise<void> {
const zone = inject(NgZone);
return (): Promise<void> => {
Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(zone)});
return zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost('devtools').catch(() => null));
return zone.runOutsideAngular(() => MicrofrontendPlatformClient.connect('devtools').catch(() => null));
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {Injectable, NgZone, OnDestroy} from '@angular/core';
import {ApplicationConfig, Handler, IntentInterceptor, IntentMessage, MessageClient, MessageHeaders, MessageInterceptor, MicrofrontendPlatform, ObservableDecorator, TopicMessage} from '@scion/microfrontend-platform';
import {ApplicationConfig, Handler, IntentInterceptor, IntentMessage, MessageClient, MessageHeaders, MessageInterceptor, MicrofrontendPlatformClient, MicrofrontendPlatformHost, ObservableDecorator, TopicMessage} from '@scion/microfrontend-platform';
import {environment} from '../environments/environment';
import {HashLocationStrategy, LocationStrategy} from '@angular/common';
import {TestingAppTopics} from './testing-app.topics';
Expand Down Expand Up @@ -67,7 +67,7 @@ export class PlatformInitializer implements OnDestroy {
}

// Log the startup progress (startup-progress.e2e-spec.ts).
MicrofrontendPlatform.startupProgress$
MicrofrontendPlatformHost.startupProgress$
.pipe(takeUntil(this._destroy$))
.subscribe({
next: progress => {
Expand All @@ -83,7 +83,7 @@ export class PlatformInitializer implements OnDestroy {

// Run the microfrontend platform as host app
await this._zone.runOutsideAngular(() => {
return MicrofrontendPlatform.startHost({
return MicrofrontendPlatformHost.start({
applications: testingAppConfigs,
activatorLoadTimeout: environment.activatorLoadTimeout,
activatorApiDisabled: activatorApiDisabled,
Expand All @@ -106,7 +106,7 @@ export class PlatformInitializer implements OnDestroy {
// Make Observables to emit in the correct zone.
Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(this._zone)});
// Connect to the host.
return this._zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost(getCurrentTestingAppSymbolicName()));
return this._zone.runOutsideAngular(() => MicrofrontendPlatformClient.connect(getCurrentTestingAppSymbolicName()));
}

private installMessageInterceptors(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,48 @@ In this Chapter
[discrete]
=== Starting the Platform in the Host Application

The host application starts the platform by calling the `MicrofrontendPlatform.startHost` method and passing the platform's configuration, which contains at minimum the web applications to be registered as micro applications. Registered micro applications can connect to the platform and interact with each other. You can also specify various timeouts and control platform behavior. For a detailed overview of platform and application configuration, see chapter <<chapter:configuration:configuring-the-platform>>.
In the host application the SCION Microfrontend Platform is configured and web applications that want to interact with the platform are registered. For a detailed overview of platform and application configuration properties, refer to chapter <<chapter:configuration:configuring-the-platform>>.

The host application starts the platform by calling the `MicrofrontendPlatformHost.start` method.

The following code snippet illustrates how to start the platform in the host application.
[source,typescript]
----
include::starting-the-platform.snippets.ts[tags=startHost1]
----
<1> Lists the micro applications able to connect to the platform to interact with other micro applications.
<1> Lists the applications allowed to interact with the platform.

The platform should be started during the bootstrapping of the host application. In Angular, for example, the platform is typically started in an app initializer. Since starting the platform host may take some time, you should wait for the startup Promise to resolve before interacting with the platform.

As with micro applications, you can provide a manifest for the host, allowing the host to contribute capabilities and declare intentions. The host manifest can be passed either as an object literal or specified as a URL to load it over the network.
The host application can provide a manifest to declare intentions and contribute behavior to integrated applications via `MicrofrontendPlatformConfig.host.manifest`. The manifest can be specified either as an object literal or as a URL to load it over the network.

[source,typescript]
----
include::starting-the-platform.snippets.ts[tags=startHost2]
----
<1> Specifies the host manifest. Alternatively, you can pass a URL to the manifest for loading it over the network.
<2> Lists the micro applications able to connect to the platform to interact with other micro applications.

When starting the platform, it loads the manifests of registered micro applications and installs activator microfrontends, if any. The method for starting the platform host returns a `Promise` that resolves once platform startup completed. You should wait for the Promise to resolve before interacting with the platform.
<2> Lists the applications allowed to interact with the platform.

[[chapter:configuration:connecting-to-the-host]]
[discrete]
=== Connecting to the Platform from a Micro Application
=== Connecting to the Platform from a Microfrontend

A micro application connects to the platform host by invoking the method `MicrofrontendPlatform.connectToHost` and passing its identity as argument. The host checks whether the connecting micro application is qualified to connect, i.e., is registered in the host application under that origin; otherwise, the host will reject the connection attempt.
A microfrontend connects to the platform host by invoking the `connect` method on `MicrofrontendPlatformClient` and passing its application identity as the argument.

The following code snippet illustrates how to connect to the platform in a micro application.
The following code snippet illustrates how to connect to the platform from a microfrontend.

[source,typescript]
----
include::starting-the-platform.snippets.ts[tags=connectToHost]
----

Optionally, you can pass an options object to control how to connect to the platform host. The method returns a `Promise` that resolves when connected to the platform host. You should wait for the Promise to resolve before interacting with the platform.
A microfrontend should connect to the platform host during application bootstrapping. In Angular, for example, this is typically done in an app initializer. Since connecting to the platform host is an asynchronous operation, the microfrontend should wait for the Promise to resolve before interacting with the platform or other microfrontends.

NOTE: The platform connects to the host through its window hierarchy. Therefore, the microfrontend must be embedded as direct or indirect child window of the host application window.

[[chapter:configuration:configuring-the-platform]]
[discrete]
=== Configuring the Platform
You configure the platform by passing following <<objects::microfrontend-platform-config,config object>> when starting the host platform. Besides listing micro applications qualified to connect to the platform, you can specify the manifest of the host application, control platform behavior, declare user-specific properties available to micro applications, and more.
You configure the platform by passing a config object when starting the platform host. Besides listing micro applications allowed to interact with the platform, you can specify the manifest of the host application, control platform behavior, declare common properties available to micro applications, and more.

[[objects::microfrontend-platform-config]]
.Properties of `MicrofrontendPlatformConfig`
Expand All @@ -72,7 +75,7 @@ You configure the platform by passing following <<objects::microfrontend-platfor
a| `<<objects::application-config,ApplicationConfig>>[]`
| yes
|
| Lists the micro applications able to connect to the platform to interact with other micro applications.
| Lists the applications allowed to interact with the platform.

See <<objects::application-config,ApplicationConfig>> for an overview of the properties.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {ApplicationConfig, MicrofrontendPlatform} from '@scion/microfrontend-platform';
import {MicrofrontendPlatformClient, MicrofrontendPlatformHost} from '@scion/microfrontend-platform';

{
async function startHost1() {
// tag::startHost1[]
MicrofrontendPlatform.startHost({
await MicrofrontendPlatformHost.start({
applications: [ // <1>
{
symbolicName: 'product-catalog-app',
Expand All @@ -14,9 +14,9 @@ import {ApplicationConfig, MicrofrontendPlatform} from '@scion/microfrontend-pla
// end::startHost1[]
}

{
async function startHost2() {
// tag::startHost2[]
MicrofrontendPlatform.startHost({
await MicrofrontendPlatformHost.start({
host: {
manifest: { // <1>
name: 'Web Shop (Host)',
Expand All @@ -39,8 +39,8 @@ import {ApplicationConfig, MicrofrontendPlatform} from '@scion/microfrontend-pla
// end::startHost2[]
}

{
async function connect() {
// tag::connectToHost[]
MicrofrontendPlatform.connectToHost('product-catalog-app');
await MicrofrontendPlatformClient.connect('product-catalog-app');
// end::connectToHost[]
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This part explains how micro applications and microfrontends can communicate wit

Cross-application communication is an integral part when implementing a microfrontend architecture. By using the browser's native `postMessage()` mechanism, you can send messages to applications loaded from different domains. For posting a message, however, you need a reference to the `Window` of the receiving application, which can quickly become complicated, also due to restrictions imposed by the <<terminology:same-origin-policy,Same-origin Policy>>.

The SCION Microfrontend Platform provides a client-side Messaging API on top of the native `postMessage` mechanism to allow microfrontends to communicate with each other easily across origins. The Messaging API offers publish/subscribe messaging to microfrontends in two flavors: <<chapter:topic-based-messaging>> and <<chapter:intent-based-messaging>>.
The SCION Microfrontend Platform provides a client-side Messaging API on top of the native `postMessage` mechanism to enable microfrontends to communicate with each other easily across origins. The Messaging API offers publish/subscribe messaging to microfrontends in two flavors: <<chapter:topic-based-messaging>> and <<chapter:intent-based-messaging>>.

NOTE: Data sent from one JavaScript realm to another is serialized with the _Structured Clone Algorithm_. The algorithm supports structured objects such as nested objects, arrays, and maps.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Handler, IntentInterceptor, IntentMessage, MessageInterceptor, MicrofrontendPlatform, PlatformState, TopicMatcher, TopicMessage} from '@scion/microfrontend-platform';
import {Handler, IntentInterceptor, IntentMessage, MessageInterceptor, MicrofrontendPlatform, MicrofrontendPlatformHost, PlatformState, TopicMatcher, TopicMessage} from '@scion/microfrontend-platform';
import {Beans} from '@scion/toolkit/bean-manager';

{
Expand Down Expand Up @@ -32,7 +32,7 @@ import {Beans} from '@scion/toolkit/bean-manager';
Beans.register(IntentInterceptor, {useClass: IntentLoggerInterceptor, multi: true}); // <2>

// Start the platform.
MicrofrontendPlatform.startHost(...); // <3>
MicrofrontendPlatformHost.start(...); // <3>
// end::message-logger-interceptor-registration[]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ In this Chapter
=== Concepts and Usage
The Intention API enables controlled collaboration between micro applications. It is inspired by the Android platform where an application can start an activity via an _Intent_ (such as sending an email).

To collaborate, an application must express an <<terminology:intention,intention>>. An intention refers to one or more <<terminology:capability,capabilities>>, or activity, in the Android platform. Capabilities can be browsed similar to a catalog and invoked by issuing an <<terminology:intent,intent>>. Manifesting intentions allows us to see dependencies between applications down to the functional level.
To collaborate, an application must express an <<terminology:intention,intention>>. An intention refers to one or more <<terminology:capability,capabilities>>, or activity, in the Android platform. Capabilities can be browsed similar to a catalog and invoked by issuing an <<terminology:intent,intent>>. Manifesting intentions enables us to see dependencies between applications down to the functional level.

====
A micro application can make functionality available to micro applications in the form of capabilities. For a micro application to browse or invoke a capability, the micro application must declare an intention in its manifest. To invoke a capability, a micro application issues an intent.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Capability, CapabilityInterceptor, Intent, IntentClient, IntentMessage, IntentSelector, ManifestObjectFilter, ManifestService, MicrofrontendPlatform} from '@scion/microfrontend-platform';
import {Capability, CapabilityInterceptor, Intent, IntentClient, IntentMessage, IntentSelector, ManifestObjectFilter, ManifestService, MicrofrontendPlatformHost} from '@scion/microfrontend-platform';
import {Beans} from '@scion/toolkit/bean-manager';

`
Expand Down Expand Up @@ -186,7 +186,7 @@ import {Beans} from '@scion/toolkit/bean-manager';

{
// tag::enable-intention-register-api[]
MicrofrontendPlatform.startHost({
MicrofrontendPlatformHost.start({
applications: [
{
symbolicName: 'product-catalog-app',
Expand Down Expand Up @@ -222,6 +222,6 @@ function hash(capability: Capability): string {
Beans.register(CapabilityInterceptor, {useClass: MicrofrontendCapabilityInterceptor}); // <1>

// Start the platform.
MicrofrontendPlatform.startHost(...); // <2>
MicrofrontendPlatformHost.start(...); // <2>
// end::register-capability-interceptor[]
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ The microservice and microfrontend architecture design approach enables us to fo
include::{terminologydir}/microfrontend.adoc[]
====

For the end-user, however, it is still a single application that he loads into his browser. The composition of the microfrontends is entirely transparent to him. By striving for a uniform look and feel of the microfrontends, the user does not even notice that different micro applications are involved.
For the user, however, it is still a single application. The composition of the microfrontends is transparent. A consistent look and feel across microfrontends further contribute to a coherent user experience.

Loading

0 comments on commit fff9953

Please sign in to comment.