Open ruanyl opened 2 years ago
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.
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:
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.raven
( crash report library, 32.3kB, minified), redux
, lodash
(or its esm version), 'axios', react
, react-dom
, rxjs
, mui
, i18-next
My questions are:
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?core-js
and why?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 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
Just to be clear, For an comprehensive example of an react-redux app, instead of the below code, https://github.com/module-federation/module-federation-examples/blob/5142c518b29e6ec94216e3c895d448cff3e2ef10/comprehensive-demo/app-01/webpack.config.js#L61-L75 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,
}
},
core-js/
bundle the polyfill that not in host app initial chunk?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.
Thank for your valuable time, I just booked a meeting in calendly. Please feel free to reschedule the meeting at your convenience @ScriptedAlchemy
Thank you! @ScriptedAlchemy Would love to see the meeting record, anywhere I can watch it? π
Didn't record. But should have. Dm me happy to do another.
Sharing all dependencies from package.json
as some of the examples show in here can also lead to an issue (https://github.com/webpack/webpack/issues/15971) where nested dependencies are resolved to an incorrect version.
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 |
---|---|
Notice in the "current" scenario, Container@2.0.0
resolved an incorrect Item@1.0.0
Apprently, base on the quote from sokra, this is not recommended:
Quoted from: https://github.com/webpack/webpack/issues/15971#issuecomment-1172163299
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).
.
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']
}
}
]
}
Hi @amoshydra, I am experiencing the same issue. Have you managed to solve it?
any update regarding this issue?
{ shared: { ...dependencies, 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
singleton
I understand it is different case by case, but would be nice to see what other people think π