-
-
Notifications
You must be signed in to change notification settings - Fork 381
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
Support Module Federation + React SSR #926
Comments
@theKashey So the issue here is that loadable is using require.resolveWeak, we need a way server-side to "flush chunks" in such a manner that the federated If we do Would it be possible to add a |
To clarify - there is no need to change existing code, only add extra explicit |
To get us unblocked, through the least path of resistance - that's the idea. While Id like to keep the discussion going, perhaps on another thread about better integration of chunk flushing between host remote pairs - we can make it functional by explicitly requiring the federated module and let webpack internally handle all the preloading that we normally have to make lodable and limitChunkCount plugin do. So if babel did something like this, just for server builds. // samples/loadable-react-16/app1/src/client/components/App.tsx
const LoadableContent = loadable(() => import('app2/Content'), {
fallback: <div>loading content...</div>,
});
if (typeof window === 'undefined') {
require('app2/Content');
} More precisely: import loadable from '@loadable/component'
const OtherComponent = loadable({
chunkName() {
return 'App1-Content' // need a solve to map this to remove federated chunk stats, but out of scope
},
isReady(props) {
if (typeof __webpack_modules__ !== 'undefined') {
return !!__webpack_modules__[this.resolve(props)]
}
return false
},
requireAsync: () =>
import(/* webpackChunkName: "OtherComponent" */
'App2/Content'),
requireSync(props) {
const id = this.resolve(props)
if (typeof __webpack_require__ !== 'undefined') {
return __webpack_require__(id)
}
return eval('module.require')(id)
},
resolve(isFederated) {
if(isFederated) {
return require('App2/Content') // the original import requested, webpack now "sees" it and will delay startup till chunk is ready In host.
}
if (require.resolveWeak) {
return require.resolveWeak('App2/Content')
}
return require('path').resolve(__dirname, 'App2/Content')
},
}) |
Perhaps a magic comment marker could be used if you need a way to parse and know the difference between a internal normal module and a remote imported module. but the idea i had for users would be loadable(import('App2/Content'), {federated: true}); Which would produce a transpiled loadable function that uses require instead of require ResolveWeak. Im not sure if requireSync also needs to be adjusted, but we should be able to test and fiddle with it quite quickly if it does require any additional adjustment. I do believe that given I can just require() it manually in the file - as long as we put |
Its been a while since my universal days, but do you have a way to know if loadable is processing common.js or browserjs builds during transform? This works for the server, but the reason we even used resolveWeak was to prevent webpack from un-splitting the client bundle. |
Ok, I’ve got the idea - Webpack should have remotes loaded before it can handle stuff contained in them. question:
|
if you can wait for the dynamic import then yeah that will work - if it needs to be "sync" like resolveWeak does - then it can be require. But if you can await that dynamic import then we can absolutely use it. Yes webpack will or should load its remotes required upfront or as needed as long as it is aware of them (sees import or require) in the tree. The issue I ran into with loadable was there's technically two parts, the remoteEntry.js file and the remote chunk you want, so webpack can hold for both but in how loadable works right now we would have to ensure there's some location where federated imports are required in some way, not weak resolved. If loadable can wait on a promise then we can just return import() otherwise we would make a sync require instead of a resolveWeak |
Would using a sync e.g. a |
Yeah a sync require would mean a blocking import - however, this has often been the case for non suspense react render tools. ResolveWeak is sync because we force the chunks back into the main chunk in order to ensure everything is there that needs to be before attempting a single render pass. We can await a async import like Kashey suggested but this is still a blocking operation because no element can be a promise once the render begins. At lululemon we are going to use EFS like a mounted volume in the VPC so MF can read a "disk" instead of fetch a string from S3, since s3 is way slower and efs has tested to take like 5ms to pull a 1mb chunk. If you're on persistent servers like docker, there's more options to tamp down this delaying response times. If we do a pre-pass render to flush out used imports, that's also an option but I'd suggest benchmarking to validate that in effort to pull modules a bit lazier, we don't end up adding more execution overhead. This is the problem with sync renders. Before we can walk the tree at all, anything that might be in the tree has to be ready. However, how I designed node federation is to behave like an async file read when require is called. So if we can use import(), the upfront cost would be reduced and when the file is actually needed - the webpack runtime can read the file on demand. We can also strike a balance on the chunking algorithms. So if preload is required, we are not loading like 50 little files - but can tell webpack to join small chunks together so there's less to pull. I'd rather pull 10 10kb files than 100 1kb files. Server side we are not as limited on network and bandwidth, so pulling say 1mb - the http handshake probably is the majority of the delay and the download is extremely fast. I'd also weigh up the longevity of sync renders at your company. Lastly, I do think wall time can be reduced quite a bit if we were to handle multiple async parts at once. Like fetch data + preload modules + some other async. Then promise all it. I've done this with nested render passes in the past and most the time it was my api calls and secrets manager that took longer. So by the time all the other parts were done, MF was already ready. In node Federation require is fs.readFile or fetch + vm.runInThisContext, so as long as the remote is loaded - requiring other code, webpack won't know the difference between reading the FS or fetching a chunk since both are promise that resolves string to webpack. Within the app it's "sync" - but what depends is how dependency management is setup, sync require is hoisted, import() is not which would give async handling back to webpack without the framework having any idea that code is being pulled by webpack runtime |
Kashey came up with the concept of preheating, the idea being you promise all some deps then on next tick, kick start the render. All this said, if we can use import() and await it in loadable, we can optimize or at least allow the application to kickstart, just not start rendering. For things like data loading that would be perfect, webpack can warm up while the application is busy fetching route level data since data must resolve before render can begin |
Hello folks, just sharing something we can add to our update. I just discovered that apollo-client found a way to create a global context and avoid duplication of contexts, this is a problem that happens with loadable in our example project here. We can use this strategy with our ChunkExtractorManager to pass the ChunkExtractor instance properly to the InnerLoadable in the withChunkExtractor function component using the ChunkExtractor.collectChunks without workarounds. |
Not sure if I follow as the following code is equal
PS: watch for line ending in your case, there is some unexpected stuff ( |
Hello, folks. Me and @ScriptedAlchemy just created a PR that adds a new option to babel-plugin called Here is a branch with the changes to test this fix. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
🐛 Bug Report
There are problems when trying to integrate the loadable with a React application that uses Module Federation, especially if the application performs SSR with federated components.
Sample to reproduce
I created a repository that exemplifies the issues in two use cases, one with React 16 and the other with React 18. Here is the repo link
https://github.com/brunos3d/loadable-module-federation-react-ssr
How to run it
To make it work just clone it, navigate with the terminal to one of the example folders, install the dependencies with yarn, and then run
yarn start
, here are the commands to run quickly:cd samples/loadable-react-16 yarn install yarn start
About the sample repo
The project has a readme file that explains how to use it, but basically, there are two workspaces in the samples folder, two applications in each workspace.
Expected behavior
In both samples the expected result is the same: render the
app1
on the server side consuming the Content component that is federated byapp2
application using Module Federation and importing it using loadable. The component must be delivered to the client side statically and must be hydrated so that it can be reactive whenever the user changes the text input.Workarounds and hacks
I worked alongside @ScriptedAlchemy so that we could identify a way to make this work and we created some workarounds in the examples to make this work. They are described/listed here, I hope that these applied hacks can be useful to solve the problem natively. The places that have the application of these workarounds are marked with the following comment
// ================ WORKAROUND ================
.The text was updated successfully, but these errors were encountered: