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.js declares main: './entrypoints/main.js', and lists it in ALWAYS_LOADED_ENTRY_POINTS. That wrapper only imports main.js, and Vite already served it, so a migration file can sit next to it as super_sidebar.vue3_migration.yml does.
  • Page entries no longer contain ./main.
  • _head.html.haml renders webpack_bundle_tag 'main' right before the page entry. The Vite layout renders the same helper, so one helper resolves main in Webpack, Rspack and Vite.
  • The default entry and the webpack_controller_bundle_tags fallback that served it are removed. A route without a page entry now gets main from the layout tag.

Load order does not change:

  • Every script tag is defer, so main still 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 main cache group (name: 'main_shared', chunks: 'all', enforce: true) takes two kinds of modules: every module in the main entry, and every module that almost every page uses. The second rule was the old 90% group, which was named main. 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 references main_shared, which the page already loaded, and does not keep its own copy.
  • enforce: true puts the modules in one chunk, so minSize and maxInitialRequests cannot split or drop it.
  • In Rspack, two more groups send main's vendor modules to the existing framework and gitlab_ui chunks, also with chunks: 'all'. Vendor code stays in the chunks that change least between deploys. Webpack 4 has no framework or gitlab_ui chunks.
  • No cache group is named main now. A cache group that shares a name with an entry merges into it. Then every page that loads the chunk would also start main, which would break a later main.vue3 swap.

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 main entry 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 on pages.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, common held 4 small modules, under the 150 KB minSize. 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 main no longer stops the page. Before, ./main and the page module started in one call, so an error thrown at the top level of main also stopped the page entry. Now they are separate scripts, and the page entry still runs.
  • Async work in main now finishes before the page code starts. Before, it finished after the page's top-level code. The only async step at the top level of main.js is the test_utils import, which runs in test environments only.
  • The bundle-size report shows renamed chunks. It reports main.chunk.js as changed and main_shared as 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 loaded main through the layout. They do not change.
  • The Vite layout loads the same entrypoints/main.js as before, now through webpack_bundle_tag.
  • No code other than the cache group config uses the default entry or the chunk name main.

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

  1. With GDK on Rspack, check out this branch and run gdk restart rspack.
  2. Open http://localhost:3808/assets/webpack/manifest.rspack.json. The main entrypoint lists runtime, gitlab_ui, framework, main_shared and main, and there is no default entrypoint.
  3. Open any project page. The page source has the main scripts 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

Edited by Miguel Rincon

Merge request reports

Loading
Loading