Refactor chunking algorithm - #2575
Conversation
4dacac6 to
587f0cb
Compare
587f0cb to
cea491a
Compare
|
@tivac: Following #2553 (comment), I added a new |
|
if i understood correctly, will this PR help to solve this issue? #2444 |
|
Yes, that should work perfectly now, just added a link to the description :) I did not go through the existing issues yet, I hope there are more that are fixed by this. |
|
@lukastaegert Looks like that did the trick! I'm now seeing |
|
@lukastaegert can't wait for this to be merged!! |
|
Well, it is doing part of #2407. What is missing is handling the namespaces internally as single variables and getting rid of the facades for dynamic imports. Not sure I will add this soon, at the moment my focus is on refactoring code and fixing issues for a 1.0 release. |
guybedford
left a comment
There was a problem hiding this comment.
Finally managed to get to an initial review... thanks for your patience.
Really great work, happy to discuss further.
| resolution.isDynamicEntryPoint && | ||
| !resolution.chunkAlias | ||
| ) { | ||
| dynamicImports.add(resolution); |
There was a problem hiding this comment.
I think we need to be doing some colouring for dynamic imports to make this algorithm work properly.
Here is a test case I tried that isn't working correctly:
main.js
import './dep1.js';
console.log('main');
dep1.js
import './dep2.js';
console.log('dep1');dep2.js
import('./dep1.js');
console.log('dep2');The dynamic import back to dep1 should be informing a colouring of dep1.js being an entry point chunk effectively.
Instead I got the following output:
main.js
console.log('main');
console.log('dep1');
import('./generated-chunk.js');generated-chunk.js
console.log('dep2');The problem with this is that it has broken the execution order invariant that we require dep2 to execute statically and synchronously before dep1 and main.
There was a problem hiding this comment.
Here's the key point from #2407 that I feel may have been missed here (correct me if I'm wrong):
it would actually be an improvement to the chunking algorithm if we could work out the common base dependencies of a given dynamic import (the intersection of the static dependency graph of all modules that use this dynamic import)
There was a problem hiding this comment.
I am sorry but I cannot follow entirely. Using your test case, I (IMO correctly) get the following two chunks:
// main.js
import './generated-chunk.js';
console.log('main');
// generated-chunk.js
Promise.resolve().then(function () { return dep1; });
console.log('dep2');
console.log('dep1');
var dep1 = /*#__PURE__*/Object.freeze({
});which looks correct to me. Is there something I am missing. Adding inlineDynamicImports, this all gets folded into one chunk correctly. One could argue that it could have been a single chunk all along but that is another point.
I must admit I also do not understand what you mean with your key point from #2407 . Maybe you can illustrate this with some examples of the form input -> expected output?
There was a problem hiding this comment.
I tried this again and you're right it's working correctly - not sure why it wasn't working in my test previously.
| exports: ChunkExports; | ||
| } = undefined; | ||
| isEmpty: boolean; | ||
| private chunkName: string | void; |
There was a problem hiding this comment.
Wasn't the chunk.id the same as the chunkName before? How are these two properties different?
There was a problem hiding this comment.
chunkName is just the "[name]" part of the chunk id and is also used by generateId() for this purpose. The idea is that the name can be used as a stable way to identify a chunk while the actual id depends on the name pattern and possibly the hash. This name is now attached to the output.
| if (!inputOptions.experimentalPreserveModules) { | ||
| if (singleInput) { | ||
| for (const chunk of chunks) { | ||
| if (chunk.entryModule === undefined) continue; |
There was a problem hiding this comment.
Should this not be replaced with chunk.entryModules.length === 0 or similar to retain the logic?
There was a problem hiding this comment.
I am not sure the old logic was correct. Now we no longer have a single chunk as soon as there is more than one chunk no matter for what reason another chunk was created.
| } | ||
| usedIds[chunk.id] = true; | ||
| } | ||
| assignChunkIds(chunks, singleChunk, inputOptions, outputOptions, inputBase, addons); |
| export interface RenderedChunk { | ||
| dynamicImports: string[]; | ||
| exports: string[]; | ||
| facadeModuleId: string | null; |
There was a problem hiding this comment.
Would isFacade not handle this same information, meaning facadeModuleId === imports[0]?
There was a problem hiding this comment.
Not necessarily. Having a facadeModuleId does not mean this is a specially created facade but that this chunk has the same interface and purpose as the given module. When only having a single entry and no dynamic imports, the facadeModuleId of the created chunk would be the entry point id. If this does not properly reflect this meaning, maybe another name would be better?
| @@ -0,0 +1,83 @@ | |||
| import ExternalModule from '../ExternalModule'; | |||
There was a problem hiding this comment.
Great to see this file separated.
| moduleById = new Map<string, Module | ExternalModule>(); | ||
| assetsById = new Map<string, Asset>(); | ||
| modules: Module[] = []; | ||
| needsTreeshakingPass: boolean = false; |
There was a problem hiding this comment.
Could you perhaps explain what this refactoring fixed in the process if there was an underlying fix with this?
There was a problem hiding this comment.
Actually there was though I no longer know the precise situation. Before, each module had its own needsTreeshakingPass. Now if a variable is declared in module 'a' but included by module 'b', it could happen that the flag would be set for module 'a' instead of module 'b'. If this module was already processed but no other modules request a new pass, it could happen that no new pass was triggered. Having a shared flag for all modules makes much more sense and fixes this kind of issue.
BTW as part of the refactorings of the other PR, I could get rid of requestTreeshakingPass on the AST context entirely and keep this logic to the module level.
| if (!dynamicImportModule.chunkAlias) | ||
| dynamicImportModule.chunkAlias = dynamicImportAliases[i]; | ||
| } | ||
| } |
There was a problem hiding this comment.
Great to see this all moved out of here.
| for (const entryModule of chunk.entryModules) { | ||
| if (chunk.facadeModule !== entryModule) { | ||
| const entryPointFacade = new Chunk(this, [], inlineDynamicImports); | ||
| entryPointFacade.turnIntoFacade(entryModule); |
| usedModules: Module[] = undefined; | ||
| id: string = undefined; | ||
| name: string; | ||
| variableName: string; |
* Give precedence to entry facades when deconflicting ids * Add entryModuleId to entry facades
…ry points * Use internal names for dynamic entry points * Do not generate dynamic entry points that are not used
used in place of the facaded module. Also no longer inline dynamic imports that are part of the static graph for now as this logic would fail if those are then dynamically imported by another chunk.
…ogic Also delete tests that have been skipped for years. Let's rather make a fresh start should the problem still persists and someone care to address it.
is complicated and may not have been properly tested in all situations.
namespace objects when inlined
c2551f1 to
ae2d424
Compare
guybedford
left a comment
There was a problem hiding this comment.
I can't say I've read all the code, but I've checked out anything that might have concerned me about this and it's an excellent refactoring.
I noted the algorithm suggestion, perhaps that comment can be copied into #2407. This will be an important one I think though because otherwise chunk size grows quite fast with dynamic import count.
| currentEntryHash = randomUint8Array(10); | ||
| modulesVisitedForCurrentEntry = { [currentEntry.id]: null }; | ||
| addCurrentEntryColourToModule(currentEntry); | ||
| } |
There was a problem hiding this comment.
Ah I missed that the dynamic colouring is being done here.
There was a problem hiding this comment.
Ok, here's an example of the algorithm I was suggesting (~> means dynamic import, -> means static import):
E1
~> D1 -> F -> T
-> S -> T
-> R
E2
~> D2 -> G -> R
-> S -> T
-> R
E3
~> D3 -> H -> S->T
-> S -> T
-> R
The current colouring algorithm will result in no optimization groups at all in the above, as every module has a unique colouring.
On the other hand, we could get a more optimal chunking since we know that S, T and R are always loaded together despite the dynamic imports being different colours:
E1
~> D1 -> F -> STR
-> STR
E2
~> D2 -> G -> STR
-> STR
E3
~> D3 -> H -> STR
-> STR
The algorithm is, for each dynamic import, to determine the intersection of the static graph of all of its importers. So for each of D1, D2 and D3 this is STR. Then when doing colouring for those dynamic imports, to skip these shared items when it comes to colouring, as the dynamic import isn't affecting their colours as they are already available.
This algorithm is also the general case of resolving the issue we have with the test case I provided:
Main -> dep1 -> dep2 ~> dep1dep1 has parent importer static intersection Main, dep1, dep2. Therefore the colouring of dep1 doesn't recolour itself and as a result we get a single chunk.
Perhaps for another day, but glad to be able to verify the logic here. Let me know if that all sounds correct too please!
There was a problem hiding this comment.
Note also here that dynamic import facades can be rendered inline:
import('./static-chunk').then(x => ({ get some () { return x.a }, get export () { return x.b }, get names () { return x.c } }));There was a problem hiding this comment.
Thanks a lot, this really helped understand it! Definitely worth adding this in the future!
Note also here that dynamic import facades can be rendered inline
Just a minor question, you are using getters instead of Object.freeze, any particular advantage or disadvantage to doing this? Or is Object.freeze semantically wrong here?
Otherwise this all really makes sense, I'll add it to the other issue.
There was a problem hiding this comment.
Ah I see, this is just another alternative to creating a namespace which does not require any changes to the exporting chunk.
| resolution.isDynamicEntryPoint && | ||
| !resolution.chunkAlias | ||
| ) { | ||
| dynamicImports.add(resolution); |
There was a problem hiding this comment.
I tried this again and you're right it's working correctly - not sure why it wasn't working in my test previously.
| this.entryModule = module; | ||
| this.isEntryModuleFacade = true; | ||
| if (module.isEntryPoint || (module.isDynamicEntryPoint && !inlineDynamicImports)) { | ||
| this.entryModules.push(module); |
There was a problem hiding this comment.
This is something I should have done a long time ago, thanks, a much richer representation.
This PR contains:
Are tests included?
Breaking Changes?
List any relevant issue numbers:
Resolves #2444,
resolves #2284,
resolves #2549
Description
Breaking change:
The
optimizeChunksoption has been renamed toexperimentalOptimizeChunksfor now as I do not feel confident presenting this as production ready. My hope is to use the changes in this PR at some point as a base to refactoroptimizeChunksto a much simpler and more stable algorithm.This supersedes and closes #2553. This PR refines the extended bundle API defined in #2553 by changing the
entryModuleIdproperty to afacadeModuleIdto stress the fact that a chunk mimics the interface of the given module. Additionally, internal dynamic imports are added to the chunk information as well.Beyond that, lots of things are changed under the hood:
Including the changes from #2553, this is what the bundle information looks like after this PR: