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
Using lazy imports seems to cause a size increase in the eagerly imported entry point. #3417
Comments
https://github.com/filipesilva/rollup-aio-lazy is empty. Can you recreate the issue on https://rollupjs.org/repl/? |
Oh gosh, I'm so sorry. I got everything together but then forgot to push! It should be there now. |
Can this be reduced at all? Creating a minimal reproduction will lead to this being fixed much quicker. You might even discover the source of the problem along the way! More info: https://git.io/fNzHA |
I can pare down the input code but only by trial and error. I'm not familiar with the mechanisms that would lead to code being moved around and how it interacts with tree shaking so it is hard to make informed changes. |
Trial and error is absolutely good enough! |
@jakearchibald here's a pared down repro, in the same repository: rollup-aio-lazyRepro for #3417. Using lazy imports seems to cause a size increase in the eagerly imported entry point. Repro steps:
// src/main.js
import { MatButtonModule } from '@angular/material/button';
console.log(MatButtonModule)
import('./lazy')
// src/lazy.js
import * as i2 from "@angular/material/tabs";
console.log(i2)
// src/main-nolazy.js
import { MatButtonModule } from '@angular/material/button';
console.log(MatButtonModule)
|
The source code is much smaller, but it's still importing large dependencies. The effect is much more dramatic than before though, going from 4k to 74k main bundle by adding a lazy module doesn't sound correct at all. |
I think this is a proper reduction. Right now, Rollup cannot split individual modules. I'm not sure any build tool can. Here's an issue for it #2512 |
Exactly, thanks for the spot-on analysis. Splitting large files is something we think about a lot but unfortunately all ideas we have so far would involve larger refactorings and are pushed back at the moment. So to rephrase the problem, you are statically importing a small export from a module and dynamically importing the remaining parts of the very same module. As Rollup cannot split a module, everything is included in the static import already. |
Note that without bundling, the same would still happen i.e. the static import would pull in the large file. |
Thank you for the explanation. I understand now. Angular ship 2 formats for ES2015 under different
Following your explanation, I expected that using the
Am I missing something here? I updated the repro to use this main field. |
Following @jakearchibald's previous reduction, I this one shows the same output even though the
Since The only way to keep them separate seems to be directly importing from |
Try manually setting moduleSideEffects to false. There could be a static import between the split up modules. Also try with rollup@2.0.0-1 if it makes a difference |
Using 2.0.0-1 moves the code used only in lazy chunks to the lazy chunk if it is in a separate module:
I also tried with my bigger repro and saw a big improvement with 2.0.0-1:
Using |
Nice, I was hoping Rollup 2 would help here. It adds side-effect detection to improve the chunk split and this is a huge testament to its usefulness. It basically ready for release, it is just waiting for a last review of the core plugins with regard to breaking changes |
Perhaps I'm not following this thread correctly... there is marked improvement with Rollup 2 regarding the placement of lazy chunks, but this particular limitation is by design? |
Yes, I understand it is. At the time I was comparing builds flat ESM vs non-flat ESM builds so I just wanted to be specific. |
Side note: there's some reliability issue with the Rollup REPL - an async race condition perhaps? If I close Firefox 73 and reopen it with this issue's URL and click on the first REPL link in #3417 (comment), the Rollup REPL will not produce any output and the web console has an error:
A subsequent reload of the same github URL and clicking on the REPL link will succeed in producing REPL output. The REPL link in #3417 (comment) has the same problem with a newly restarted Firefox 73 instance. Edit: To reproduce the REPL issue, the Firefox cache must be cleared completely upon restart or a private window must be launched. |
I'm still not clear on the intended behavior of dynamic imports in Rollup. Does Rollup 2 assume that all dynamic imports are opaque and could have side effects? Is Rollup 2 expected to retain |
Yes, because there is still no detection in Rollup 2 which properties are used or unused in a dynamic import. What Rollup 2 does is ignore empty imports if it cannot detect a side-effect in the imported module and it uses this to create more efficient chunks. More details here: #3369 |
Thanks for the clarification that there's no technical obstacle, and it's just a matter of implementation. In my interpretation of the overview in #3369 it suggests that Rollup already deduces and records whether a given module is side effect free without the use of a |
True, the problem is that they are part of the public interface and it is not tracked yet in how far exports are used, which is a non-trivial thing to get right. |
E.g. import('foo'); // no exports need to be included
import('foo').then(() => console.log('loaded')); // no exports need to be included
import('foo').then(({bar}) => console.log(bar)); // at least bar must be included
import('foo').then(ns => console.log(ns.bar)); // at least bar must be included
const importPromise = import('foo');
importPromise.then(({bar}) => console.log(bar)); // at least bar must be included
const logBar = ({bar}) => console.log(bar);
import('foo').then(logBar); // at least bar must be included etc. This would be easier if we had object shape tracking first, which is a big thing I have been pushing away in favour of other things for some time. Also it would help to implement a deeper understanding of Promises into Rollup as well. |
I had a look but the error seems to be deep inside Svelte. Maybe a Svelte bug, but hard to track in any case what is causing it, at least for me. |
That sounds hard! I wonder if we might get there sooner via https://github.com/tc39/proposal-module-attributes#import-statements: import { foo, bar } from './modules.js' as 'js-promise'; …where |
Maybe. I'm not sure I like module attributes but let's keep looking at this, I see a big danger that this will increase the syntactic gap between browsers and bundlers rather than making it smaller. I think at some point we will get to handling the cases I outlined correctly, at least in theory I have ideas where to start for those, but it is rather long-term. Still, existing code-bases would profit from this very much. |
The module attributes proposal does not appear to aid dynamic Object shape tracking would be the gold standard, but even without it I think some of the dynamic import scenarios could be special-cased and handled with a little effort - particularly the unused return value scenario, and the immediate |
My suggestion was that it could be used instead of dynamic import, since it's easier to statically analyse. I agree that object shape tracking with regular |
I thought that lazy imports require the use of dynamic |
There's a dumb but effective heuristic that could be employed without too much effort to eliminate many of the exports of dynamically Edit: It might be prudent to include String |
What about more complex computed properties? const x = 3;
import('foo').then(ns => console.log(ns[`value${x}`])); |
But I must say the idea is quite ingenious. Also, we not only need properties from the calling modules but from all modules as namespaces could be used in other modules. And if a namespace itself is just handed to an external callback, we are out of luck. |
You don't see many such computed properties for dynamic import lookups in practise in the wild. There's a reason why people embraced statically analyzable import/export to avoid that sort of thing. But this dynamic module pruning could be an opt-in if that's a concern. |
I suppose that extra level of safety could be offered as a conservative option, but we might be able to trap such cases automatically. Limiting analysis to the calling module has the advantage of reducing the accidental property collision space resulting in less dynamically imported code needing to be bundled and/or chunked.
It might be possible to escape certain dynamic import reference usages to include all exported symbols in the worst case scenario. Warnings could also be produced by Rollup in cases where this is happening to inform the user why certain dynamically imported code is being retained. Not 100% foolproof, but it should do the trick for most projects - particularly if the heuristic is an opt-in. |
The proposal is: import { foo, bar } from './modules.js' as 'something'; Where 'something' is an instruction for how to interpret the module. Browsers would understand certain values (like 'json' and 'css'), but build tools could have their own. I was suggesting that Rollup would transpile: import { foo, bar } from './modules.js' as 'js-promise'; to const foo = import('./modules.js').then(m => m.foo);
const bar = import('./modules.js').then(m => m.bar); The benefit of using a static import is you don't need object shape tracking, as import statements can be statically analysed. But yeah, if object shape tracking is rock solid, a workaround like this isn't needed. |
@jakearchibald I see what you mean now. I originally thought you were proposing a drop-in replacement for dynamic Rather than extending an ES proposal, it would be relatively straightforward to have Rollup recognize an annotation for dynamic import to state precisely which exports it is interested in. Such a hint would be backwards compatible with browsers and require minimal code change:
This hypothetical rollup annotation could also support wildcard patterns. |
It just occurred to me that an annotation is not necessary for dynamic import if Rollup could recognize destructuring declaration assignments and drop unused exports in the const {foo, bar} = await import("module"); |
True, just supporting this pattern might be something we could support with rather low impact for now. |
To recap, I think these are all the simple deterministic dynamic import patterns for side-effect-free modules:
If all dynamic imports match one of the forms above for a given module, then it's just an additive set of exports that are to be retained. Any other dynamic import usage would result in retaining all exports in the module. On a related note, any idea why
|
Because the right side has a side-effect and there is no logic yet to replace declarations with their side-effects in such a case. Just a note: This discussion has nothing to do with the original issue other than it has something to do with dynamic imports. I would suggest to move it to a new issue. |
I'm happy enough to have the issue hijacked really. I wasn't aware that there wasn't used/unused property detection in dynamic imports and that's definitely part of the ideal treeshaking story. |
This will be my last post on the issue. After rescanning the thread I'm not sure what the resolution was - it seemed to be more of a discovery process. I honestly thought my points about side-effect-free dynamic imports were on-topic and advancing the goal of this project.
|
they definitely are. However the issue here was not which named exports are included in the dynamically imported module. The issue is a different one: The main chunk was importing something from a file that had itself a dependency on the dynamically imported file (or one of its direct dependencies) even though nothing was USED from that dependency by the main chunk. Still following strict execution dynamics, the code that was included by the dynamic import was now also included in the main chunk because previously, Rollup did not know too much about WHY code was included. In the new version, however, the unused import will be removed from the main chunk if Rollup can be sufficiently certain that there are no side-effects. And hence it is attributed to the lazy chunk. Detecting the actually used imports is also useful in its own right, but would have done nothing to solve the original issue. |
The original PR explains this in a little more detail with examples: #3369 |
This issue has been resolved via #4952 as part of rollup@3.21.0. You can test it via |
Ah, I think this is not actually resolved. At the core, this was about splitting modules that are shared between dynamic importers and import targets. Will reopen. |
Actually, looking at the conversation, it is solved, I just do not think that that was exactly the original issue... |
Rollup Version: 1.32.0
Operating System (or Browser): Windows 10
Node Version (if applicable): v12.4.0
Link to reproduction (IMPORTANT, read below):
git clone https://github.com/filipesilva/rollup-aio-lazy
cd rollup-aio-lazy
yarn
yarn rollup --dir dist-full
src/app/custom-elements/element-registry.js
and comment out this part:yarn rollup --dir dist-nolazy
dist-full
anddist-nolazy
, see the full version is ~50kb larger:File: Compare Active File With...
to see a diff between the two. Some of the extra retained symbols, likeFocusKeyManager
andA11yModule
, are declared inmain
, unused there, exported, and only used in one of the lazy modules.Expected Behavior
I expected that adding lazy imports would not add extra code to the eager code.
Actual Behavior
Adding lazy imports brought in extra code that should be in the lazy chunks.
The text was updated successfully, but these errors were encountered: