feat: chunk importmap#16552
Conversation
|
|
|
This fixes such a gigantic performance issue in almost every vite app out there - thanks a lot! Hopefully this can get merged soon 👀 |
|
So this PR has not been merged yet, right? |
|
Is there any way to ask the maintainers to review this? It's a shame that such a useful PR is open for over 4 months without any reviews... |
|
What is the simplest way to consume this code manually, until (if ever) it gets merged? |
This ☝ |
1a0eaf5 to
ef5d053
Compare
ef5d053 to
9bed85b
Compare
There was a problem hiding this comment.
Thanks for working on this! Sorry that it took time to review.
While I think it needs some small things to polish, the approach looks good to me. I think the remaining tasks are to think about how this works with experimental.renderBuiltUrl and think about the interaction with plugins.
| ## html.chunkImportMap | ||
|
|
||
| - **Type:** `boolean` | ||
| - **Default:** `false` | ||
|
|
||
| Whether to inject importmap for generated chunks. This importmap is used to optimize caching efficiency. | ||
|
|
There was a problem hiding this comment.
It seems build.chunkImportMap is used instead of html.chunkImportMap in the code.
I think build.chunkImportMap is better as I think we should have an option to generate the import map in a separate file so that frameworks and users using backend integration can use it.
Let's move this description to docs/config/build-options.md.
There was a problem hiding this comment.
Might even be worth being an experimental feature. build.experimentalChunkImportMap
Im really looking forward to this change as itll be a massive win for users.
I expect though at this time there may be gotchas we need to learn and patch, as people report behaviour issues with there individual setups.
There was a problem hiding this comment.
I think we can set experimental without changing the name. 👍
There was a problem hiding this comment.
Nice, I need to use this feature as backend integration 👍 . It would be great if we could export ImportMap contents into a file like import-maps.json, and the consumers can name as the build.manifest feature (ref: https://vite.dev/config/build-options.html#build-manifest)
| return [ | ||
| base + augmentFacadeModuleIdHash(output.preliminaryFileName), | ||
| base + output.fileName, | ||
| ] |
There was a problem hiding this comment.
📝 Note to reviewers: the mapping by the browsers is operated after both the module specifier and the key of import maps are resolved to absolute URLs.
https://html.spec.whatwg.org/multipage/webappapis.html#import-maps:~:text=Such%20remappings%20operate,app.mjs%22
| config.root, | ||
| normalizePath(filename), | ||
| ) | ||
| const assetsBase = getBaseInHTML(relativeUrlPath, config) |
There was a problem hiding this comment.
I guess this does not take experimental.renderBuiltUrl into account. For non-runtime ones, we can generate a static import map. But for runtime ones, I guess we would need to output an script that constructs an import map.
There was a problem hiding this comment.
I haven’t come up with a good way to use it together with the renderBuiltUrl runtime yet.
Considering that we need to handle the import map from other plugins, we’ll need some kind of clever solution.
Are there any other options that could be used as a reference?
I’ll need some time to investigate and think it over.
| @@ -519,6 +566,16 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { | |||
| } | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
When build.chunkImportMap is enabled, I guess the application won't work if the browser does not support import maps but supports all features included in detectModernBrowserDetector.
I think we need to add if(HTMLScriptElement&&HTMLScriptElement.supports("importmap"))throw new Error; to detectModernBrowserDetector and modernChunkLegacyGuard.
There was a problem hiding this comment.
I tried simply adding an if statement at the end of detectModernBrowserDetector, but it doesn't seem to have worked 👀
It seems that __vite_legacy_guard inside modernChunkLegacyGuard is not being executed from anywhere.
Could you explain how it works?
There was a problem hiding this comment.
It seems that
__vite_legacy_guardinsidemodernChunkLegacyGuardis not being executed from anywhere.
Could you explain how it works?
The current modernChunkLegacyGuard uses the fact that browsers errors without executing a script if the script has invalid syntax (unsupported syntax). But in this case, we are judging with runtime code instead of a syntax. So I was suggesting adding the code in modernChunkLegacyGuard (i.e. changing modernChunkLegacyGuard to `if(HTMLScriptElement&&HTMLScriptElement.supports("importmap"))throw new Error;`);export function __vite_legacy_guard(){${detectModernBrowserDetector}};`.
That said, I noticed that the script that has the throw new Error won't run but still the scripts imported by that script would run. Hmm...
| const chunkImportMap = config.build.chunkImportMap | ||
| ? createChunkImportMap(bundle) | ||
| : {} |
There was a problem hiding this comment.
Probably we need to think about how to let other plugins access this information. I assume other plugins will need to access this information sometimes.
| Object.values(meta.chunks).forEach((chunk) => { | ||
| const hashPlaceholder = chunk.fileName.match(hashPlaceholderRE)?.[0] | ||
| if (!hashPlaceholder) return | ||
| if (hashPlaceholderToFacadeModuleIdHashMap.get(hashPlaceholder)) return | ||
|
|
||
| hashPlaceholderToFacadeModuleIdHashMap.set( | ||
| hashPlaceholder, | ||
| getHash(chunk.facadeModuleId ?? chunk.fileName), | ||
| ) | ||
| }) | ||
|
|
||
| const codeProcessed = augmentFacadeModuleIdHash(code) | ||
| return { | ||
| code: codeProcessed, | ||
| map: new MagicString(codeProcessed).generateMap({ | ||
| hires: 'boundary', | ||
| }), |
There was a problem hiding this comment.
📝 For reviewers: This works as rollup calculates the chunk hash after the renderChunk hooks are run since Rollup 3.
Reference: https://www.youtube.com/watch?v=cFwO9UvDzfI
|
Thank you for the review ✨ |
|
I’ve pushed what I have for now!
I need more time 🧐 |
|
|
||
| ### Chunk importmap | ||
|
|
||
| Creating an import map for chunks helps prevent the issue of cascading cache invalidation. This import map features a list of stable file IDs linked to filenames with content-based hashes. When one chunk references another, it utilizes the file ID instead of the content-hashed filename. As a result, only the updated chunk needs cache invalidation in the browser, leaving intermediary chunks unchanged. This strategy enhances the cache hit rate following deployments. |
There was a problem hiding this comment.
| Creating an import map for chunks helps prevent the issue of cascading cache invalidation. This import map features a list of stable file IDs linked to filenames with content-based hashes. When one chunk references another, it utilizes the file ID instead of the content-hashed filename. As a result, only the updated chunk needs cache invalidation in the browser, leaving intermediary chunks unchanged. This strategy enhances the cache hit rate following deployments. | |
| Creating an import map for chunks helps prevent the issue of cascading cache invalidation. This import map features a list of stable file IDs linked to filenames with content-based hashes. When one chunk references another, it utilizes the file ID instead of the content-hashed filename. As a result, only the updated chunk needs cache invalidation in the browser, leaving intermediary chunks unchanged. This strategy enhances the cache hit rate following deployments. | |
| To enable this feature, set [`build.chunkImportMap`](/config/build-options.md#build-chunkimportmap) to `true`. |
| ## html.chunkImportMap | ||
|
|
||
| - **Type:** `boolean` | ||
| - **Default:** `false` | ||
|
|
||
| Whether to inject importmap for generated chunks. This importmap is used to optimize caching efficiency. | ||
|
|
There was a problem hiding this comment.
I think we can set experimental without changing the name. 👍
| ## build.chunkImportMap | ||
|
|
||
| - **Type:** `boolean` | ||
| - **Default:** `false` |
There was a problem hiding this comment.
| - **Default:** `false` | |
| - **Default:** `false` | |
| - **Experimental** |
| @@ -519,6 +566,16 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { | |||
| } | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
It seems that
__vite_legacy_guardinsidemodernChunkLegacyGuardis not being executed from anywhere.
Could you explain how it works?
The current modernChunkLegacyGuard uses the fact that browsers errors without executing a script if the script has invalid syntax (unsupported syntax). But in this case, we are judging with runtime code instead of a syntax. So I was suggesting adding the code in modernChunkLegacyGuard (i.e. changing modernChunkLegacyGuard to `if(HTMLScriptElement&&HTMLScriptElement.supports("importmap"))throw new Error;`);export function __vite_legacy_guard(){${detectModernBrowserDetector}};`.
That said, I noticed that the script that has the throw new Error won't run but still the scripts imported by that script would run. Hmm...
| config.root, | ||
| normalizePath(filename), | ||
| ) | ||
| const assetsBase = getBaseInHTML(relativeUrlPath, config) |
|
I think cache is an important issue on the web. Are there any updates? |
|
Are there any updates on this PR? The poor caching performance at the moment is blocking us from using Vite in production. |
|
@bhbs we've been running an older version of these changes for a while in production - and it works almost perfectly. However we've noticed one issue recently with it. I was waiting to create a reproduction for you to look at, but I haven't found the time to do it - so I figure it's probably better to describe the issue now, and maybe it'll make sense to you without a reproduction. The issue has to do with Async Chunk Loading Optimization, basically the In our codebase, we've seen cases where some changes can cause a chunk that has a It doesn't seem to ever cause user-visible errors - beyond causing a lot of 404s since we're trying to preload files that do not exist in the importmap, and of course this means that prefetching won't work, leading to slower app loads. Let me know if that is useful information. |
|
@jonomuller Just to clarify and avoid any misunderstanding, this cascading cache invalidation issue also occurs with other tools—Webpack (Next.js), Rollup, esbuild, etc. The fact that this issue has yet to be fully resolved should not be a reason not to use in production 😀 (That said, if an OSS solution already exists that addresses this problem, I would love to learn about it.) |
|
@bhbs Thanks for the reply! At least for our use case (coming from a CRA app using Webpack and migrating to Vite), CRA/Webpack seemed to have a solution for this at runtime that did not use importmaps, which then surprised us when switching over to Vite. This article provides a good example of how it works. Using your solution solves the problem, but we've been unable to use it in production due to browser compatibility issues with |
|
@jonomuller |
|
In this PR, I aim to tackle the issue in a more elegant way using an import map. With Webpack, for example, when the map is contained in the entry JS file, changes to a terminal chunk file require reloading the HTML, the entry file, and the terminal chunk file. However, by utilizing an import map, you only need to reload the HTML and the terminal chunk file. And, we need to consider browser compatibility issues as well.
I apologize for the delay—I’ve been busy with personal matters lately, so I’d be grateful for any contributions from others. |
Do you need help getting this across the finish line? Personally would love to help — have an issue with this occurring in Astro when I'm loading long-term cached documents that reference Javascript and CSS files that have been renamed with new hashes on rebuild. Would love to help fix my this problem for Vite. |
|
Importmap can be loaded from external file. |
|
In legacy browsers, this polyfill can be used to support Importmap. |
It seems that many people have been requesting improvements in chunk cache efficiency, so I've created this PR. Please take a look and review this! Thank you, Vite team!
Description
Fixes: #6773
PoC: #15373
Discussion: #15372
Creating an importmap for chunks helps prevent cascading cache invalidation. This importmap features a list of filenames with file ID-based hashes linked to filenames with content-based hashes. When one chunk references another, it uses the ID-hashed filename instead of the content-hashed filename. As a result, only the updated chunk requires cache invalidation in the browser, leaving intermediary chunks unchanged. This strategy enhances the cache hit rate following deployments.
Implementation
During the renderChunk step, file hash placeholders in the code are overwritten with stable ID hashes. This process also maintains a record of the hash placeholders and their corresponding ID hashes. (After that, during the augmentHash step, filenames are assigned based on the content.)
Based on the generated chunk list and the record of hash placeholders and their corresponding hashes, an importmap is generated and inserted into the HTML.
Others