Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] What's the best practices of shared dependencies? #2219

Open
ruanyl opened this issue Aug 24, 2022 · 9 comments
Open

[Question] What's the best practices of shared dependencies? #2219

ruanyl opened this issue Aug 24, 2022 · 9 comments
Labels
question Further information is requested

Comments

@ruanyl
Copy link

ruanyl commented Aug 24, 2022

  1. Include all dependencies in shared
const { dependencies } = require('./package.json');

{
  shared: {
    ...dependencies,
    react: {
      singleton: true,
      requiredVersion: dependencies['react'],
    },
    'react-dom': {
      singleton: true,
      requiredVersion: dependencies['react-dom'],
    },
  },
}

This seems will end up with too many chunks, feels a bit overwhelming as it leads to a lot more requests.

  1. Only include necessary ones, like those need to be singleton and those large dependencies
{
  shared: {
    component-library: {
      requiredVersion: '^x.x.x',
    },
    '@apollo/client': {
      requiredVersion: '^x.x.x',
    },
    react: {
      singleton: true,
      requiredVersion: dependencies['react'],
    },
    'react-dom': {
      singleton: true,
      requiredVersion: dependencies['react-dom'],
    },
  },
}

Then we would need to carefully sync the required versions so that each federated module compatible with each other

  1. Only include those need to be singleton
{
    react: {
      singleton: true,
      requiredVersion: dependencies['react'],
    },
    'react-dom': {
      singleton: true,
      requiredVersion: dependencies['react-dom'],
    },
  },
}

I understand it is different case by case, but would be nice to see what other people think 😃

@ScriptedAlchemy
Copy link
Member

Sharing should be done with care - since shared modules cannot be tree shaken.

If you need a singleton (like things that depend on react context), then it must be shared.

Sharing all dependencies can lead to larger bundles so its best to consider case by case.

Example:
I have a icon package; if I share it - ill have 5000 icons downloaded - it would make more sense to just let each remote download some duplicated icons.

If I share myIconLib/ then each icon can be chunked, so I wont download a 25mb file - but my remote entry will now have 5000 keys in its share scope making the remote about 5mb
So I don't share icons, I just let them download again as needed.

@ScriptedAlchemy ScriptedAlchemy added the question Further information is requested label Aug 29, 2022
@icy0307
Copy link

icy0307 commented Aug 29, 2022

We are facing the same problem trying to use module federation in real world.
What should be done to pick out those shared modules is very tricky.

Before module federation, in the extremely complicated app we have,
we got an host app,which show the main content of the app, and 10+ remote apps loaded after "first rendering", in idle time during page initialization process.
Those remote apps have their own build process and loaded via script tags.

Because of that, those remote apps bundle their own dependency like core-js and axios, which lead to multiple copies of these libraries to be loaded in the page initialization process.

Our performance tab shows EvaluateScript cause 35% of time, when the page is loading, this is why we turned to module federation. (There is an alternative, that remote apps not longer load through scrip tags directly, but by npm packages that won't bundle their dependencies. What do you think?)

In order to render the first content that visible to user (apart from ssr),
those common chunks are required:

  1. polyfill related:
    tslib, @babel/runtime, core-js, core-js-pure(some library use it to prevent polluting global scope ).
    tslib is not big itself.
    core-js-pure is big, but an esm module that absolutely need be tree-shaken, which leads to another problem. Like I mentioned before, there are 10+ 'remote apps' are needed during open phrase in our app. So in the worst situation, at least 11+ copies of Array.prototype.includes are included.
    The most tricky one is core-js. if it is not be treated as shared modules, 10+ remote apps means at least 11+ copy of core-js are loaded during open process.
    But if it is shared , core-js is big(200Kb+), but babel ensures that only the used polyfills are included.
    Putting core-js in shared modules means even if only a few polyfills are needed in order to show the first paint , all of polyfills are loaded.
  2. other tool library
    raven( crash report library, 32.3kB, minified), redux, lodash(or its esm version), 'axios', react, react-dom, rxjs, mui, i18-next

My questions are:

  1. what should be done with esm libraries when sharing?
    Should rxjs, mui, core-js be shared? Chunks still have common parts of esm libraries, what can we do about that?
  2. Is smallest initial chunk size contradictory to smallest overall chunks size? Is there any code we could follow in order to get best of both of them?
  3. what should I do with core-js and why?
  4. what should I do with redux and i18-next?
    In your article webpack-5-module-federation-a-game-changer-in-javascript-architecture, something called AutomaticModuleFederationPlugin is mentioned, could you elaborate on that?
    @ScriptedAlchemy

@ScriptedAlchemy
Copy link
Member

ESM has not been a problem for me, automatic federation plugin is very out of date and does not cater for package exports resolution

putting a trailing slash on end of package helps ensure it deal with dynamic exports like package.json exports field.

MFP works with ESM targets too if you had a esm output set and i use esm npm packages be default without any issues as of yet

Redux is context based, should be singleton, translations likely the same and needs to be singleton too

Corejs, id share since everyone would need those and probbably the same ones over and over so sharing it is a good idea

core-js/ with trailing slash is how id share simething like that so only the used polyfills are shared and imported, not the whole index.js file

@icy0307
Copy link

icy0307 commented Aug 30, 2022

Just to be clear,
For an comprehensive example of an react-redux app,
instead of the below code,

shared: {
...deps,
'@material-ui/core': {
singleton: true,
},
'react-router-dom': {
singleton: true,
},
'react-dom': {
singleton: true,
},
react: {
singleton: true,
},
},

the correct way to do this is?

const deps = require('./package.json').dependencies;
...
 shared: { 
   'core-js/': { 
     requiredVersion: deps.core-js,
   }, 
   'lodash-es/': { 
     requiredVersion: deps.lodash-es,
   }, 
   '@reduxjs/toolkit': { 
     singleton: true, 
   }, 
   'react-dom': { 
     singleton: true, 
   }, 
   react: { 
     singleton: true, 
   }, 
   i18next: { 
     singleton: true, 
   }, 
  '@mui/material/': {
     requiredVersion: deps.@mui/material,
    }
 },
  1. What if I split chunk in my app(host app), would core-js/ bundle the polyfill that not in host app initial chunk?
  2. Sometimes, host app and remote apps are not in the same repo. The remote apps may be used in different host apps. If host app1 wanna share the same store, and host app2 need store isolation, how to handle this situation? @ScriptedAlchemy

@ScriptedAlchemy
Copy link
Member

Consider booking a call. This is tricky to document.

Free of charge.
I'll record the session and post it back here for others.

Easier to talk than write.
I cannot read very well so it's difficult to keep track of the words.

https://calendly.com/d/dwg-3cb-75w/private-session

@icy0307
Copy link

icy0307 commented Aug 30, 2022

Thank for your valuable time, I just booked a meeting in calendly. Please feel free to reschedule the meeting at your convenience @ScriptedAlchemy

@ruanyl
Copy link
Author

ruanyl commented Aug 31, 2022

Thank you! @ScriptedAlchemy Would love to see the meeting record, anywhere I can watch it? 👀

@ScriptedAlchemy
Copy link
Member

Didn't record. But should have. Dm me happy to do another.

@amoshydra
Copy link

amoshydra commented Jan 5, 2024

Sharing all dependencies from package.json as some of the examples show in here can also lead to an issue (webpack/webpack#15971) where nested dependencies are resolved to an incorrect version.

Illustration

Below is an illustration of the environment and the issue I've encountered:

.
└── node_modules/
    ├── @my-component/container@^2.0.0/
    │   └── node_modules/
    │       └── @my-component/item@^2.0.0
    └── @my-component/item@^1.0.0

webpack.config.js

shared: {
  ...packageJson.dependencies,
  // 
  // "@my-component/container": "^2.0.0",
  // "@my-component/item": "^1.0.0"
}

App.js

import Container from "@my-component/container";
import Item from "@my-component/item";

export default () => (
  <>
    <Container />
    <Item />
  </>
)
Expected Current
image image

Notice in the "current" scenario, [email protected] resolved an incorrect [email protected]

Apprently, base on the quote from sokra, this is not recommended:

Quoted from: webpack/webpack#15971 (comment)

This configuration:

shared: {
  uuid: "^8.3.2",
  // or the equivalent:
  uuid: { requiredVersion: "^8.3.2" },
}

will force the version to be ^8.3.2.
But it's not recommended to do that. You should prefer to let webpack automatically infer the requiredVersion from the nearest package.json (from import location).

.

My thought

As many readers will refer to the examples here, I would think it is best to replace all examples showing:

const deps = require('../package.json').dependencies;

module.exports = {
  shared: {
    ...deps,
    react: {
      singleton: true,
      requiredVersion: deps.react,
    },
    'react-dom': {
      singleton: true,
      requiredVersion: deps['react-dom']
    }
  }
}

to something like this?:

const deps = require('../package.json').dependencies;

module.exports = {
  shared: [
    ...Object.keys(deps),
    {
      react: {
        singleton: true,
        requiredVersion: deps.react,
      },
      'react-dom': {
        singleton: true,
        requiredVersion: deps['react-dom']
      }
    }
  ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants