module-federation / module-federation-examples

Implementation examples of module federation , by the creators of module federation
https://module-federation.io/
MIT License
5.65k stars 1.75k forks source link

How can I lazy load remoteEntry.js #681

Closed caoyi5225 closed 3 years ago

caoyi5225 commented 3 years ago

I see remote app start loading after all the remoteEntry.js loaded if i put all the remotes to host's webpack config. image

And in angular11-microfrontends, there is a method named loadRemoteEntry to load remoteEntry.js when I need it. But I should add the method to my project.

Is there any original webpack config to enable lazy load remoteEntry.js when I import the remote app? Just like this:

new ModuleFederationPlugin({
  remotes: {
    "microfeeg-app-child1": {
      url: "microfeeg_app_child1@/microfeeg-app-child1/remoteEntry.js",
      lazy: true,
     }
  },
}),

Thanks for replay.😁

ronaldohoch commented 3 years ago

Can you please, provide the route, the webpack.config.js and webpack.config.js from remote?

Because, when i run the angular11-microfrontends, it doesn't load the remoteEntry, only when i access the url of the MFE.

dgpedro commented 3 years ago

Hi. I came across with similar issue... I'm trying to load each route of a main app from remotes, however when page loads all remoteEntry.js from all routes are fetched. Maybe it's not possible to lazy load remoteEntry.js, or probably I'm doing something wrong here :/

I have 4 apps:

In main app, that's how I've defined in webpack.config.js:

        new ModuleFederationPlugin({
            name: 'main',
            filename: 'remoteEntry.js',
            remotes: {
                'app_a': 'app_a@/app-a/remoteEntry.js',
                'app_b': 'app_b@/app-b/remoteEntry.js',
                'homepage': 'homepage@/homepage/remoteEntry.js',
            },
            shared: ['react', 'react-dom'],
        }),

and the routes are defined as:

import React, {Suspense, lazy} from "react";
import {Switch, Route} from "react-router";
import {BrowserRouter, NavLink} from "react-router-dom";

const AppA = lazy(() => import('app_a/AppA'));
const AppB = lazy(() => import('app_b/AppB'));
const Homepage = lazy(() => import('homepage/Homepage'));

export const App = () => {
    return (
        <main>
            <BrowserRouter>
                <nav>
                    <div><NavLink to="/">Main</NavLink></div>
                    <div><NavLink to="/app-a">AppA</NavLink></div>
                    <div><NavLink to="/app-b">AppB</NavLink></div>
                </nav>
                <hr />
                <Suspense fallback={<div>Loading...</div>}>
                    <Switch>
                        <Route path="/app-a"><AppA/></Route>
                        <Route path="/app-b"><AppB/></Route>
                        <Route><Homepage/></Route>
                    </Switch>
                </Suspense>
            </BrowserRouter>
        </main>
    );
};

export default App;

Then for each remote app, I have in webpack.config.js something like:

        new ModuleFederationPlugin({
            name: 'app_a',
            library: { type: 'var', name: 'app_a' },
            filename: 'remoteEntry.js',
            exposes: {
                './AppA': './src/app',
            },
            shared: ['react', 'react-dom'],
        }),

When main app launches, I see in network that all 3 remoteEntry.js (homepage, app-a and app-b) are fetched: image

So following the original question, I would like to know if it's possible to have those remoteEntry.js files lazy loaded, and if yes if you could point out what I might be doing wrong.

Thanks in advance.

ronaldohoch commented 3 years ago

I don't know react, but your routes are lazyload?

https://linguinecode.com/post/code-splitting-react-router-with-react-lazy-and-react-suspense

dgpedro commented 3 years ago

Hey. Thanks for your answer.

Yes, I'm lazy loading all routes by using React.lazy and Suspense. In fact, the js of each one of the routes only gets downloaded when I navigate to the correspondent route. However, the removeEntry.js of each one of the routes is fetched always.

I've checked and tried these two examples:

Both also use lazy loading routes, and both have the same issue I'm talking about... all remoteEntry.js from all apps are fetched when host app loads.

I'm not sure if there's another example that I can look at? Or maybe it's not possible to archive lazy loading of remoteEntry.js in react?

Thanks.

ScriptedAlchemy commented 3 years ago

Look at dynamic system host or check my external-remotes-plugin under my git repo

pf-costa commented 3 years ago

Sorry for posting on a closed thread 🙏

I've tried the dynamic system example and used the external-remotes-plugin. However, at the end, I wasn't very satisfied with the end result:

Given the huge benefits of module federation and dynamic imports (lazy loading), is there anything planned in the roadmap to have this out of the box through configuration? Like it was suggested in the first post.

Thanks again for this awesome technology!

popuguytheparrot commented 2 years ago

Why issues is close? Why not make it async for non-blocking main tread?

script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
needAttach && document.head.appendChild(script);
ScriptedAlchemy commented 2 years ago

If you're dynamic importing the remote element then webpack will only add the remote when used if you're using static imports then only the low level apis (dynamic system host) will work.

popuguytheparrot commented 2 years ago

i use React.lazy, but webpack call remoteEntry when app is loading. Request to remoteEntry is blocking main tread.

ScriptedAlchemy commented 2 years ago

Use promise new promise interface on the plugin and you can inject and resolve it however you wish

popuguytheparrot commented 2 years ago

How are the promises? Which plugin? I just indicate the path to the remoteEntry.js in the webpack module federation and that's it. How to access the behavior of the plugin?

popuguytheparrot commented 2 years ago

Why issues is close? Why not make it async for non-blocking main tread?

script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
needAttach && document.head.appendChild(script);

this code from webpack

gauravmalik1011 commented 2 years ago

Hey. Thanks for your answer.

Yes, I'm lazy loading all routes by using React.lazy and Suspense. In fact, the js of each one of the routes only gets downloaded when I navigate to the correspondent route. However, the removeEntry.js of each one of the routes is fetched always.

I've checked and tried these two examples:

Both also use lazy loading routes, and both have the same issue I'm talking about... all remoteEntry.js from all apps are fetched when host app loads.

I'm not sure if there's another example that I can look at? Or maybe it's not possible to archive lazy loading of remoteEntry.js in react?

Thanks.

I am also seeing same issue where all remoteEntry files are getting loaded on page irrespective of page url. please suggest if there is any way to fix this

ScriptedAlchemy commented 2 years ago

If you don't like how webpack injects remotes. Use promise new promise syntax and take over the whole injection and resolution mechanics.

I load my remotes upfront, asynchronously since they are only like 2-5kb

Compared to the 700kb tag manager it's a moot point personally.

If you reference the import and execute it in any way at all. It's going to load the remote. You'd have to do something like make a function that calls the import() so it only fires when called. Not when module is executed

gauravmalik1011 commented 2 years ago

Thanks @ScriptedAlchemy..I was able to achieve this using promise based script injection

Will try to further improve bundle size for remoteEntry file as not allowing this file to get stored in browser cache

ScriptedAlchemy commented 2 years ago

Storing in browser cache does present a challenge. You'd need to know the version of the other remote and then you could add that to the remote module name. But it's still tricky.

You could make a api call to like a json file to get the remote name and then inject it with a path that you now know

skypyxis commented 2 years ago

Use promise new promise syntax and take over the whole injection and resolution mechanics.

Thanks @ScriptedAlchemy..I was able to achieve this using promise based script injection

Could you please provide an example / code snippet to illustrate how this works?

I have something like const mfA = React.lazy(async () => import('a-mf/App'));

but even if a don't use mfA anywhere the remoteEntry is being called.

I'm already using the ExternalTemplateRemotesPlugin to load the MF from different endpoints per environment

remotes: {
   'a-mf': 'a@[window.ENV.MF_A]/remoteEntry.js',
   'b-mf': 'b@[window.ENV.MF_B]/remoteEntry.js'
}
ScriptedAlchemy commented 2 years ago

Sounds like a config issue or you have another nested remote using this.

That externals plugin probably only supports trying to read and load the remote containers instantly. Use promise new promise.

Read the webpack docs

skypyxis commented 2 years ago

Sounds like a config issue or you have another nested remote using this.

That externals plugin probably only supports trying to read and load the remote containers instantly. Use promise new promise.

Read the webpack docs

Created a new repo with a simplified codebase but the remoteEntry.js files are always loaded ahead, even with the "promise new promise".

Code repo: https://github.com/skypyxis/react-ts-module-federation

Does anyone have a repo where the remoteEntry.js is lazy loaded?

ScriptedAlchemy commented 2 years ago

Don't set them as eager shared. That might cause webpack to eagerly negotiate share scope between the remotes

NsdHSO commented 2 years ago

@ScriptedAlchemy @skypyxis I have the same situation, two apps loading toghether here I have code source, I need your experience, Please can you help me?

krutoo commented 2 years ago

Sounds like a config issue or you have another nested remote using this.

That externals plugin probably only supports trying to read and load the remote containers instantly. Use promise new promise.

Read the webpack docs

@ScriptedAlchemy Can you provide external option example value for implement lazy loading of remote entry?

In this example repo (https://github.com/krutoo/module-federation) after i added the "shared" option, promise began to be called immediately at the start of the page

without specifying the "shared" option, the promise is only fired when a dynamic import is directly called in the code

ScriptedAlchemy commented 2 years ago

Upon looking into the webpack core code. Webpack will try/want to initialize all remotes known to it upfront. If you want to prevent that. You can create a proxy trap with promise new promise, basically resolve nothing and perform init manually when get() is fired.

krutoo commented 2 years ago

Upon looking into the webpack core code. Webpack will try/want to initialize all remotes known to it upfront. If you want to prevent that. You can create a proxy trap with promise new promise, basically resolve nothing and perform init manually when get() is fired.

@ScriptedAlchemy can you please provide reference example?

NsdHSO commented 2 years ago

Upon looking into the webpack core code. Webpack will try/want to initialize all remotes known to it upfront. If you want to prevent that. You can create a proxy trap with promise new promise, basically resolve nothing and perform init manually when get() is fired.

Yeah, after I read more about this feature, I saw this is a feature, not a bug, and I remediate this feature exactly how you said. Thanks.

ScriptedAlchemy commented 2 years ago

Yeah. It's to ensure shared module negotiations happen across the whole network that can only happen during boot up.

barabaiiika commented 2 years ago

Upon looking into the webpack core code. Webpack will try/want to initialize all remotes known to it upfront. If you want to prevent that. You can create a proxy trap with promise new promise, basically resolve nothing and perform init manually when get() is fired.

@ScriptedAlchemy, Hey. Thanks for your answers. Proxy should be something like this?

new Promise(resolve => {
  const scriptElement = document.createElement('script');

  scriptElement.onload = () => {
    const container = window['app_2_var'];
    const proxy = {
      shareScope: {},
      get: async (request) => {
        try {
          await container.init(this.shareScope);
        } catch (e) {
          console.log('remote container already initialized');
        }
        return container.get(request);
      },
      init: (arg) => {
        this.shareScope = arg; // save reference to share scope
      }
    }
    resolve(proxy);
  }

  scriptElement.src = 'http://example.com/path/to/remoteEntry.js'';
  scriptElement.async = true;

  document.head.append(scriptElement);
})
ScriptedAlchemy commented 2 years ago

Yeah, but - rather use something like this

https://github.com/module-federation/nextjs-mf/blob/main/src/utils.js

OR if you need it in webpack runtime code.

https://github.com/module-federation/nextjs-mf/blob/main/src/NextFederationPlugin.js - look at my remote template builder + you need the AddRuntimeRequirementsToPromiseExternal plugin that I add on here to make webpakc_Require accessible to promise new promise.

Dont write your own script loader, trrrrust me.

If you want, consider porting over these parts to module-federation/utilities and making a PR. Ill happily publish it to npm and point my own packages to depend on those utilities :)

Ill do it at some point, just don't have the time to extract the useful tools and move them to the other repo.

https://github.com/module-federation/utilities

nathan-iag commented 2 years ago

Hey @ScriptedAlchemy - would moving to one of the proposed solutions (e.g. https://github.com/module-federation/utilities) break the Dashboard Plugin and it's ability to know which MFE a consumer is dependent on? My understanding of the Dashboard Plugin is it reads the (remotes) configuration of the ModuleFederation Plugin.

Also, do you think this behaviour of loading remoteEntry.js on startup/bootstrap of the Host will change to on-demand in ModuleFederation V2? It seems a little counter to load all remoteEntry.js files on startup when the MFE in question might not be used until a later flow, or not at all. We use React.Lazy combined with React.Suspense to ensure all the actual components are "lazy" / loaded on-demand.

I realise remoteEntry.js is small (so long as eager is not enabled on deps) and static, so there shouldn't be too many issues, but we've noticed if a request for a remoteEntry,js takes a long time to respond (server is available but responding very slowly/hanging) it can cause the Host app to stall. This can likely be resolved by using infra to cache remoteEntry.js, but it would be good to have the on-demand loading behaviour be the default.

ScriptedAlchemy commented 2 years ago

Yeah static assets show go to a CDN instead of serving from internal infra

42shadow42 commented 1 year ago

I know this is closed, but I've encountered application delays caused by MFE's remoteEntries loading slowly even when they aren't used yet (lazy loaded) I've considered the option of dynamic remotes and got it working using this import logic:

export const MFEComponent = (url, name, path) => {
  return lazy(() =>
    new Promise((resolve, reject) => {
      __webpack_require__.l(
        url,
        (event) => {
          if (typeof window[name] !== 'undefined') return resolve(window[name])
          var errorType =
            event && (event.type === 'load' ? 'missing' : event.type)
          var realSrc = event && event.target && event.target.src
          reject({
            message:
              'Loading script failed.\n(' + errorType + ': ' + realSrc + ')',
            name: 'ScriptExternalLoadError',
            type: errorType,
            request: realSrc,
          })
        },
        name
      )
    }).then(async (remote) => {
      await __webpack_init_sharing__('default')
      await remote.init(__webpack_share_scopes__.default)
      const factory = await remote.get(`./${path}`)
      return factory()
    })
  )
}

However the remotes I am attempting to load are statically known, and supported by typescript, so a basic import of:

export const MFEComponent = (url, name, path) => {
  return lazy(() =>
    import(`MFE/ENTRYPOINT`)
  )
}

Would enable typescript support for these remote components. Any chance we can change the code generation of static imports to generate something similar to the above?