Build main as its own entry (Vue 3 migration)
What does this MR do and why?
main.js runs on every page, but it had no bundle of its own. config/helpers/entry_points.js declared it as default: ['./main'], and generateEntries prepended that to every page entry. Because of that, the vue3_migration.yml switch, which swaps <entry> for <entry>.vue3 per request, could never reach main, and every app main.js mounts stayed on Vue 2.
This MR makes main an ordinary entry:
entry_points.jsdeclaresmain: './entrypoints/main.js', and lists it inALWAYS_LOADED_ENTRY_POINTS. That wrapper only importsmain.js, and Vite already served it, so a migration file can sit next to it assuper_sidebar.vue3_migration.ymldoes.- Page entries no longer contain
./main. _head.html.hamlrenderswebpack_bundle_tag 'main'right before the page entry. The Vite layout renders the same helper, so one helper resolvesmainin Webpack, Rspack and Vite.- The
defaultentry and thewebpack_controller_bundle_tagsfallback that served it are removed. A route without a page entry now getsmainfrom the layout tag.
Load order does not change:
- Every script tag is
defer, somainstill runs before the page entry. optimization.runtimeChunk: 'single'gives all entries one module registry, so shared modules still run once.
Chunking
Page entries no longer contain main. So the bundlers no longer know that main's modules are already on the page, and they would copy those modules into page chunks and lazy chunks. The cache groups prevent this:
- One
maincache group (name: 'main_shared',chunks: 'all',enforce: true) takes two kinds of modules: every module in themainentry, and every module that almost every page uses. The second rule was the old 90% group, which was namedmain. It is in both the Webpack 4 and Rspack configs. chunks: 'all'also covers lazy chunks. A lazy chunk that imports one of these modules referencesmain_shared, which the page already loaded, and does not keep its own copy.enforce: trueputs the modules in one chunk, sominSizeandmaxInitialRequestscannot split or drop it.- In Rspack, two more groups send
main's vendor modules to the existingframeworkandgitlab_uichunks, also withchunks: 'all'. Vendor code stays in the chunks that change least between deploys. Webpack 4 has noframeworkorgitlab_uichunks. - No cache group is named
mainnow. A cache group that shares a name with an entry merges into it. Then every page that loads the chunk would also startmain, which would break a latermain.vue3swap.
Build output, before and after
| Before | After | |
|---|---|---|
main entrypoint in the manifest |
None. ./main is the first module of every page entry |
runtime, gitlab_ui, framework, main_shared, main in Rspack. runtime, main_shared, main in Webpack |
default entrypoint |
Served routes with no page entry | Removed. The layout tag loads main on every page |
Chunk with main's modules and the 90% modules |
main.chunk.js, 130.4 KB gzip in Rspack |
main_shared.chunk.js, 130.0 KB gzip in Rspack |
main.chunk.js |
The 90% shared chunk | Startup code for the main entry only, 0.1 KB |
framework / gitlab_ui (Rspack) |
200.5 KB / 38.9 KB | Same |
| Page entry chunks | Contain ./main |
Do not contain ./main |
| Script tags per page | Page entry | main, then the page entry. Files that both use are rendered once |
Non-page entries (sentry, performance_bar, jira_connect_app, graphql_explorer, redirect_listbox, sandboxed mermaid and swagger, duo_panel) |
Load main.chunk.js |
Load main_shared instead, 0.3 KB smaller each in Rspack |
tracker, coverage_persistence |
No change |
Bundle size
I built master (4d84ffe1) and this branch with each bundler. For every page entry, I compared what the layout downloads: tracker, super_sidebar, main (this branch only) and the page, de-duplicated like webpack_entrypoint_paths. "Rollout pages" are the 341 .vue3 page entries, which a page serves only when its own flag is on.
yarn build uses Webpack unless ENABLE_RSPACK=true is set, so the Webpack numbers describe the default production build.
Rspack, production build, gzip:
| Per page | min | median | p90 | max |
|---|---|---|---|---|
| 581 pages | −0.2 KB | −0.2 KB | −0.2 KB | −0.1 KB |
| 341 rollout pages | −0.2 KB | −0.2 KB | −0.1 KB | −0.1 KB |
- Every page has one more script tag: the
mainentry chunk, which is 0.1 KB of startup code. - The median page downloads 612 KB gzip on
master. - Modules that are in more than one chunk: 4,235, the same as on
master. - Lazy chunks: 100.5 MB raw, against 100.3 MB on
master.
Webpack 4, production build without minification, raw bytes. A minified build ran out of memory on my machine:
| Per page | min | median | p90 | max | sum |
|---|---|---|---|---|---|
| 581 pages | −425.7 KB | −15.4 KB | +1.0 KB | +32.4 KB | −7.3 MB |
| 341 rollout pages | −15.6 KB | 0.0 KB | +1.0 KB | +394.5 KB | −0.4 MB |
- Modules that are in more than one chunk: 4,972, against 5,197 on
master. - Lazy chunks: 172.4 MB raw, against 175.5 MB on
master. - One rollout page,
pages.groups.merge_requests.vue3, grows by 394.5 KB raw. The next largest growth is 32.4 KB raw onpages.search.show.
Why one rollout page grows in Webpack
Webpack splits a page entry into at most maxInitialRequests: 20 files. 127 page entries already use the full budget on master. When an entry is at the limit, Webpack chooses which shared chunks it keeps, and a change to the module graph changes that choice. This MR changes the graph for every page, because main leaves the page entries.
pages.groups.merge_requests.vue3 loads 21 files on master, on this branch, and before the cache group fix. Its modules, for example issuable_default_client.js, were in a shared chunk that is now outside its budget, so they stay in the page chunk.
Raising maxInitialRequests would only move the limit, so this MR does not do it.
How the cache groups changed during review
The first version of this MR kept the 90% group as a separate common group, with chunks: 'initial' on the main groups. Against master:
- In Rspack,
commonheld 4 small modules, under the 150 KBminSize. Rspack dropped the chunk and copied the modules into about 190 page chunks: +1.1 KB gzip on the median page. - In both bundlers, lazy chunks copied
main's modules: +7.5 MB raw in Rspack and +29.5 MB raw in Webpack.
The single main_shared group with chunks: 'all' removes both effects.
Side effects
- Cache miss on deploy. Every chunk hash changes, so every user downloads the shared chunks again once.
- An error in
mainno longer stops the page. Before,./mainand the page module started in one call, so an error thrown at the top level ofmainalso stopped the page entry. Now they are separate scripts, and the page entry still runs. - Async work in
mainnow finishes before the page code starts. Before, it finished after the page's top-level code. The only async step at the top level ofmain.jsis thetest_utilsimport, which runs in test environments only. - The bundle-size report shows renamed chunks. It reports
main.chunk.jsas changed andmain_sharedas added, although the code inside is almost the same.
These stay the same:
- Layouts that do not render
_head.html.haml(jira_connect,errors,oauth_error,service_desk, mailers) never loadedmainthrough the layout. They do not change. - The Vite layout loads the same
entrypoints/main.jsas before, now throughwebpack_bundle_tag. - No code other than the cache group config uses the
defaultentry or the chunk namemain.
Not in this MR
This MR adds no migration file for main, so main stays on Vue 2. The loaders no longer reject one: they treat main like any other global bundle. !261499 (merged), stacked on this MR, adds entrypoints/main.vue3_migration.yml with its flag, and the blocklist the flag needs.
How to set up and validate locally
- With GDK on Rspack, check out this branch and run
gdk restart rspack. - Open
http://localhost:3808/assets/webpack/manifest.rspack.json. Themainentrypoint listsruntime,gitlab_ui,framework,main_sharedandmain, and there is nodefaultentrypoint. - Open any project page. The page source has the
mainscripts right before the page entry's scripts.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
References
- Phase 3 of #623999 — Vue 3 migration of global bundles and
main - Written from scratch instead of reviving the earlier draft !252290 (closed)
- Followed by !261499 (merged), stacked on this MR: the blocklist and the
vue3_migrate_mainflag - Review of the cache groups: !261548 (comment 4006233233) and !261548 (comment 4006233288)
- Replaces the runtime-switch approach explored in !261461 (closed) and !261501 (closed)