perf(build): avoid quadratic link scan in the preload helper - #23510
Merged
Merged
Conversation
The `__vitePreload` runtime helper checked whether each dependency was already preloaded by scanning every <link> in the document, backwards, once per unseen dependency. For a call with L links and U unseen dependencies that is O(U * (L + U)), since the helper appends a link for every miss. A large code-split app whose dynamic imports carry long dependency lists pays it on the main thread at boot. Collect the existing link hrefs into two Sets (any rel for scripts, stylesheet only for CSS) and dedupe in O(1) per dependency, so a call costs O(L + U). The sets are built on the first dependency not yet in `seen`, so a call whose dependencies are all seen does no link work, as before. `seen` already guarantees a dependency reaches the dedupe at most once, so links the helper creates are not written back into the sets. Behaviour is unchanged: same links, same order, same CSS load promises and rejection, same vite:preloadError path, same nonce handling. Adds a playground regression test for the case where the scan matches an HTML modulepreload link. The entry already imports chunk.js, so a codeSplitting group gives it its own chunk and a modulepreload link in the built HTML; the dynamically loaded `about` module imports it too, so its preload dep list would re-hint it. The test asserts a single modulepreload link for that chunk before and after the dynamic import. It passes on main as well, since the stock scan dedupes it too, and guards the rewrite: it fails when the Set lookup or the lazy scan is removed. The config change is scoped to the dist/normal suite; the sibling suites keep their own configs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adapt expected snapshot to new src length
This was referenced Oct 1, 2026
Merged
renovate Bot
added a commit
to andrei-picus-tink/auto-renovate
that referenced
this pull request
Oct 3, 2026
| datasource | package | from | to | | ---------- | ------- | ----- | ----- | | npm | vite | 8.3.0 | 8.3.2 | ## [v8.3.2](https://github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small-832-2026-10-01-small) ##### Bug Fixes - **build:** preload CSS correctly when `renderBuiltUrl` returns URLs with queries ([#23611](vitejs/vite#23611)) ([64e0a21](vitejs/vite@64e0a21)) - **bundled-dev:** serve lazy chunk sourcemaps ([#23026](vitejs/vite#23026)) ([eb7aa9a](vitejs/vite@eb7aa9a)) - **bundled-dev:** serve the rolldown runtime from the installed rolldown ([#23568](vitejs/vite#23568)) ([bc598a6](vitejs/vite@bc598a6)) - **deps:** update all non-major dependencies ([#23601](vitejs/vite#23601)) ([9944fa6](vitejs/vite@9944fa6)) - **deps:** update rolldown-related dependencies ([#23602](vitejs/vite#23602)) ([88c1741](vitejs/vite@88c1741)) - **html:** resolve percent-encoded srcset urls ([#23609](vitejs/vite#23609)) ([53f1ce7](vitejs/vite@53f1ce7)) - limit size of object and array printing via `forwardConsole` ([#23565](vitejs/vite#23565)) ([e64a587](vitejs/vite@e64a587)) - merge `build.rolldownOptions.output.minify` correctly ([#23536](vitejs/vite#23536)) ([bba3bb8](vitejs/vite@bba3bb8)) - **optimize-deps:** avoid "unsupported" warnings for browser:false mappings ([#23590](vitejs/vite#23590)) ([5e4b9ca](vitejs/vite@5e4b9ca)) - **optimizer:** preserve excluded optional peer require fallbacks ([#23600](vitejs/vite#23600)) ([a2bd6fa](vitejs/vite@a2bd6fa)) - pass queries to `renderBuiltUrl` ([#23586](vitejs/vite#23586)) ([744269e](vitejs/vite@744269e)) - **server:** handle file watcher errors without crashing ([#23503](vitejs/vite#23503)) ([6894f5c](vitejs/vite@6894f5c)) - **server:** release previous environments after initialization ([#23499](vitejs/vite#23499)) ([5a3a010](vitejs/vite@5a3a010)) - **ssr:** encode whitespace in module runner sourceURL ([#23513](vitejs/vite#23513)) ([bbc8812](vitejs/vite@bbc8812)) - **worker:** align worker urls in client and server when using terser ([#23614](vitejs/vite#23614)) ([24bd331](vitejs/vite@24bd331)) ##### Performance Improvements - avoid encoding intermediate source maps ([#23461](vitejs/vite#23461)) ([89574f6](vitejs/vite@89574f6)) - **build:** avoid quadratic link scan in the preload helper ([#23510](vitejs/vite#23510)) ([cf5c028](vitejs/vite@cf5c028)) - only register time middleware when debug logging is enabled ([#23621](vitejs/vite#23621)) ([94d0080](vitejs/vite@94d0080)) ##### Documentation - fix dead og-image PNG links in vite6/vite7 changelog entries ([#23594](vitejs/vite#23594)) ([1929b4c](vitejs/vite@1929b4c)) ##### Miscellaneous Chores - **deps:** update vitest monorepo to v5 ([#23604](vitejs/vite#23604)) ([24339f4](vitejs/vite@24339f4)) ##### Code Refactoring - use `tinyexec` instead of `cross-spawn` ([#23583](vitejs/vite#23583)) ([db915e5](vitejs/vite@db915e5)) ##### Tests - **bundled-dev:** accept a rolldown dev runtime with no helper imports ([#23606](vitejs/vite#23606)) ([634745d](vitejs/vite@634745d)) ## [v8.3.1](https://github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small-831-2026-09-24-small) ##### Bug Fixes - **deps:** update all non-major dependencies ([#23482](vitejs/vite#23482)) ([3c752c8](vitejs/vite@3c752c8)) - **deps:** update all non-major dependencies ([#23537](vitejs/vite#23537)) ([e8990c4](vitejs/vite@e8990c4)) - **deps:** update rolldown-related dependencies ([#23483](vitejs/vite#23483)) ([9aecbbf](vitejs/vite@9aecbbf)) - handle `server.ws: false` in mergeConfig ([#23511](vitejs/vite#23511)) ([f68c0d5](vitejs/vite@f68c0d5)) - merge `build.rolldownOptions.output.comments` correctly ([#23514](vitejs/vite#23514)) ([4aba8d8](vitejs/vite@4aba8d8)) - **optimizer:** don't skip imports whose binding starts with type ([#23540](vitejs/vite#23540)) ([39330f4](vitejs/vite@39330f4)) - **optimizer:** resolve pending discovered dep processing on close before init ([#23567](vitejs/vite#23567)) ([5f89433](vitejs/vite@5f89433)) - **server:** avoid reinitializing watcher when adding file after server close ([#23572](vitejs/vite#23572)) ([6f831f9](vitejs/vite@6f831f9)) - **sourcemap:** skip URL source roots when injecting sources content ([#23519](vitejs/vite#23519)) ([04fc30a](vitejs/vite@04fc30a)) ##### Miscellaneous Chores - merge prereleases in changelog ([#23466](vitejs/vite#23466)) ([99bd9d1](vitejs/vite@99bd9d1)) - **optimizer:** add debug log when waiting for dep before init ([#23566](vitejs/vite#23566)) ([63567c7](vitejs/vite@63567c7)) - update `optimizeDeps.include` comment ([#23489](vitejs/vite#23489)) ([6a84c72](vitejs/vite@6a84c72)) ##### Code Refactoring - assets regexp use non-capture ([#23491](vitejs/vite#23491)) ([f4b4431](vitejs/vite@f4b4431)) - remove duplicate configurations ([#23532](vitejs/vite#23532)) ([9abd99b](vitejs/vite@9abd99b)) - replace `find` with `some` ([#23554](vitejs/vite#23554)) ([af7cdf6](vitejs/vite@af7cdf6))
renovate Bot
added a commit
to andrei-picus-tink/auto-renovate
that referenced
this pull request
Oct 4, 2026
| datasource | package | from | to | | ---------- | ------- | ----- | ----- | | npm | vite | 8.3.0 | 8.3.2 | ## [v8.3.2](https://github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small-832-2026-10-01-small) ##### Bug Fixes - **build:** preload CSS correctly when `renderBuiltUrl` returns URLs with queries ([#23611](vitejs/vite#23611)) ([64e0a21](vitejs/vite@64e0a21)) - **bundled-dev:** serve lazy chunk sourcemaps ([#23026](vitejs/vite#23026)) ([eb7aa9a](vitejs/vite@eb7aa9a)) - **bundled-dev:** serve the rolldown runtime from the installed rolldown ([#23568](vitejs/vite#23568)) ([bc598a6](vitejs/vite@bc598a6)) - **deps:** update all non-major dependencies ([#23601](vitejs/vite#23601)) ([9944fa6](vitejs/vite@9944fa6)) - **deps:** update rolldown-related dependencies ([#23602](vitejs/vite#23602)) ([88c1741](vitejs/vite@88c1741)) - **html:** resolve percent-encoded srcset urls ([#23609](vitejs/vite#23609)) ([53f1ce7](vitejs/vite@53f1ce7)) - limit size of object and array printing via `forwardConsole` ([#23565](vitejs/vite#23565)) ([e64a587](vitejs/vite@e64a587)) - merge `build.rolldownOptions.output.minify` correctly ([#23536](vitejs/vite#23536)) ([bba3bb8](vitejs/vite@bba3bb8)) - **optimize-deps:** avoid "unsupported" warnings for browser:false mappings ([#23590](vitejs/vite#23590)) ([5e4b9ca](vitejs/vite@5e4b9ca)) - **optimizer:** preserve excluded optional peer require fallbacks ([#23600](vitejs/vite#23600)) ([a2bd6fa](vitejs/vite@a2bd6fa)) - pass queries to `renderBuiltUrl` ([#23586](vitejs/vite#23586)) ([744269e](vitejs/vite@744269e)) - **server:** handle file watcher errors without crashing ([#23503](vitejs/vite#23503)) ([6894f5c](vitejs/vite@6894f5c)) - **server:** release previous environments after initialization ([#23499](vitejs/vite#23499)) ([5a3a010](vitejs/vite@5a3a010)) - **ssr:** encode whitespace in module runner sourceURL ([#23513](vitejs/vite#23513)) ([bbc8812](vitejs/vite@bbc8812)) - **worker:** align worker urls in client and server when using terser ([#23614](vitejs/vite#23614)) ([24bd331](vitejs/vite@24bd331)) ##### Performance Improvements - avoid encoding intermediate source maps ([#23461](vitejs/vite#23461)) ([89574f6](vitejs/vite@89574f6)) - **build:** avoid quadratic link scan in the preload helper ([#23510](vitejs/vite#23510)) ([cf5c028](vitejs/vite@cf5c028)) - only register time middleware when debug logging is enabled ([#23621](vitejs/vite#23621)) ([94d0080](vitejs/vite@94d0080)) ##### Documentation - fix dead og-image PNG links in vite6/vite7 changelog entries ([#23594](vitejs/vite#23594)) ([1929b4c](vitejs/vite@1929b4c)) ##### Miscellaneous Chores - **deps:** update vitest monorepo to v5 ([#23604](vitejs/vite#23604)) ([24339f4](vitejs/vite@24339f4)) ##### Code Refactoring - use `tinyexec` instead of `cross-spawn` ([#23583](vitejs/vite#23583)) ([db915e5](vitejs/vite@db915e5)) ##### Tests - **bundled-dev:** accept a rolldown dev runtime with no helper imports ([#23606](vitejs/vite#23606)) ([634745d](vitejs/vite@634745d)) ## [v8.3.1](https://github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small-831-2026-09-24-small) ##### Bug Fixes - **deps:** update all non-major dependencies ([#23482](vitejs/vite#23482)) ([3c752c8](vitejs/vite@3c752c8)) - **deps:** update all non-major dependencies ([#23537](vitejs/vite#23537)) ([e8990c4](vitejs/vite@e8990c4)) - **deps:** update rolldown-related dependencies ([#23483](vitejs/vite#23483)) ([9aecbbf](vitejs/vite@9aecbbf)) - handle `server.ws: false` in mergeConfig ([#23511](vitejs/vite#23511)) ([f68c0d5](vitejs/vite@f68c0d5)) - merge `build.rolldownOptions.output.comments` correctly ([#23514](vitejs/vite#23514)) ([4aba8d8](vitejs/vite@4aba8d8)) - **optimizer:** don't skip imports whose binding starts with type ([#23540](vitejs/vite#23540)) ([39330f4](vitejs/vite@39330f4)) - **optimizer:** resolve pending discovered dep processing on close before init ([#23567](vitejs/vite#23567)) ([5f89433](vitejs/vite@5f89433)) - **server:** avoid reinitializing watcher when adding file after server close ([#23572](vitejs/vite#23572)) ([6f831f9](vitejs/vite@6f831f9)) - **sourcemap:** skip URL source roots when injecting sources content ([#23519](vitejs/vite#23519)) ([04fc30a](vitejs/vite@04fc30a)) ##### Miscellaneous Chores - merge prereleases in changelog ([#23466](vitejs/vite#23466)) ([99bd9d1](vitejs/vite@99bd9d1)) - **optimizer:** add debug log when waiting for dep before init ([#23566](vitejs/vite#23566)) ([63567c7](vitejs/vite@63567c7)) - update `optimizeDeps.include` comment ([#23489](vitejs/vite#23489)) ([6a84c72](vitejs/vite@6a84c72)) ##### Code Refactoring - assets regexp use non-capture ([#23491](vitejs/vite#23491)) ([f4b4431](vitejs/vite@f4b4431)) - remove duplicate configurations ([#23532](vitejs/vite#23532)) ([9abd99b](vitejs/vite@9abd99b)) - replace `find` with `some` ([#23554](vitejs/vite#23554)) ([af7cdf6](vitejs/vite@af7cdf6))
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hey
I found this little opportunity while looking at browser performance traces originally, I've run this patch in prod with measured (inline in my patch) improvement metrics across the population. Sharing in case it makes sense for everyone.
Before
The
__vitePreloadruntime helper checked whether each dependency was already preloaded by scanning every<link>in the document, backwards, once per unseen dependency. For a call with L links and U unseen dependencies that is O(U * (L + U)), since the helper appends a link for every miss. A large code-split app whose dynamic imports carry long dependency lists pays it on the main thread at boot.After
Collect the existing link hrefs into two Sets (any rel for scripts, stylesheet only for CSS) and dedupe in O(1) per dependency, so a call costs O(L + U). The sets are built on the first dependency not yet in
seen, so a call whose dependencies are all seen does no link work, as before.seenalready guarantees a dependency reaches the dedupe at most once, so links the helper creates are not written back into the sets.Behaviour is unchanged: same links, same order, same CSS load promises and rejection, same vite:preloadError path, same nonce handling.
Performence measurement
A large code-split app ships this change as a patch currently. The helper times its own main-thread cost per editor boot, and on a small random sample of page loads it also replays the removed per-dependency link scan read-only, so the same boot records what the stock scan would have cost on that device. Traffic across desktop and mobile browsers; about 900
<link>s in the document after boot.__vitePreloadcalls per boot; times are summed over them