Load the Duo Chat panel from its own bundle

What does this MR do and why?

ee/app/assets/javascripts/main_ee.js called initDuoPanel() at module scope. main_ee.js is imported by main.js. main.js is compiled into every page bundle. So the Duo Chat panel's module graph shipped on every page. That graph has about 74 Vue components. It is the largest single block in main.

The call did nothing on most pages. #duo-chat-panel is only present when the user has Duo access. So every user without Duo access downloaded the panel's code for no reason.

This MR moves the panel into its own webpack entry. Users without Duo access no longer download it.

Approach

The panel gets its own entry, duo_panel. The script tag for it comes from the partial that owns the panel's markup, ee/app/views/layouts/_duo_chat_panel.html.haml.

DuoChatPanel::Component#render? already returns false when the user has no Duo access, or when Duo is locked off at the instance level. The partial captures whether the component produced output. It emits the bundle tag only when it did. This ties the bundle to the DOM it needs, without duplicating the access logic.

Load order stays the same in practice. app/helpers/gitlab_script_tag_helper.rb sets defer on every script tag. Deferred scripts run in document order. main is emitted from layouts/_head and still runs first. The panel still initialises after the document is parsed, exactly as it did before. webpack_entrypoint_paths de-duplicates shared chunks across tags within a request. So the panel tag only adds the chunks the page does not already have.

The entry lives in CE, not EE. config/helpers/entry_points.js is a CE file. The entry must resolve in a FOSS build. app/assets/javascripts/entrypoints/duo_panel.js imports through ee_else_ce. CE gets a no-op initDuoPanel stub at app/assets/javascripts/ai/init_duo_panel.js. This mirrors how main_ee.js itself is stubbed in CE. FOSS never requests the bundle, because the EE partial does not exist there.

Six files change, +36/-5:

  • new entry: app/assets/javascripts/entrypoints/duo_panel.js
  • new CE stub: app/assets/javascripts/ai/init_duo_panel.js
  • config/helpers/entry_points.js: the duo_panel entry, and duo_panel in ALWAYS_LOADED_ENTRY_POINTS. spec/frontend/config/entry_points_spec.js requires every bundle a layout loads to be in that list, and the infection scanner uses the list as the global seeds for its duplicated-modules check, the same way performance_bar is listed although it is conditional
  • removed import and call in ee/app/assets/javascripts/main_ee.js
  • updated ee/app/views/layouts/_duo_chat_panel.html.haml
  • two new cases in the partial's view spec

How to verify

  • bundle exec rspec ee/spec/views/layouts/_duo_chat_panel.html.haml_spec.rb — 8 examples, 0 failures. Two are new: the bundle tag is emitted when the panel renders, and is not emitted for an anonymous user.
  • bundle exec rspec ee/spec/components/duo_chat_panel/component_spec.rb — 48 examples, 0 failures.
  • yarn jest ee/spec/frontend/ai/init_duo_panel_spec.js — 47 tests pass.
  • Full yarn rspack-prod build on the rebased branch: exits 0. duo_panel.<hash>.chunk.js is emitted (570,838 bytes) and main.<hash>.chunk.js shrinks from 1,173,629 to 454,114 bytes, a 61% reduction. manifest.rspack.json lists duo_panel with the same shared chunks as the page entries plus its own, so on a page that renders the panel webpack_entrypoint_paths adds only the panel's chunks. The bundle-size bot on the first pipeline reported the same shape: mainChunk −2.95 MB (−40.9%) uncompressed.
  • ESLint, Prettier, RuboCop, and haml-lint are clean on the changed files.

Browser check

Done through the existing feature spec ee/spec/features/duo_chat/duo_chat_drawer_spec.rb (SaaS context, "opens the drawer to chat with GitLab Duo"), run against this commit on GDK with Vite. A temporary assertion checked that the page carried a script[src*="duo_panel"] tag, then the panel was opened from the toggle and captured:

Group page with the GitLab Duo Chat panel open, loaded from the duo_panel bundle

The panel renders and takes input, so the bundle loads and initialises in the right order. Sending a message needs a live AI gateway and was not exercised. The negative path is covered by the view spec: no duo_panel script tag for a user without Duo access.

Notes

This supersedes the ?vue3 approach in the merge request linked below. That approach put a feature-flagged dynamic import inside main_ee.js. With the panel in its own bundle, it can use the ordinary vue3_migration.yml rollout instead. Its Vue 3 migration becomes independent of main's migration.

This MR targets master and does not depend on !252286 (merged). The two can merge in either order.

Follow-ups, not in this MR:

  • Once !252286 (merged) is merged, add app/assets/javascripts/entrypoints/duo_panel.vue3_migration.yml with status: rollout and a vue3_migrate_duo_panel flag. That is the whole Vue 3 rollout for the panel.
  • The entry and its CE stub could live in ee/. That needs a conditional entry in config/helpers/entry_points.js, support in config/helpers/vite_plugin_page_entrypoints.mjs for a global entry outside the CE entrypoints/ directory, and an ee/ prefix in the entry index that !252286 (merged) adds. There is no EE-only global bundle today, so this is kept out of this MR and the stub follows the main_ee.js precedent.
  • entrypoints/duo_panel.js imports ~/commons, like super_sidebar and performance_bar. Under a ?vue3 rollout that import duplicates commons/* into the Vue 3 lane. The issue linked below tracks removing the import from the performance bar; the same question applies here when the panel migrates.

References

  • #623999 — tracking issue; this is Phase 2 of that plan
  • !252041 (closed) — the ?vue3 prototype this supersedes
  • !252286 (merged) — Phase 1, the rollout mechanism the panel can use once both are merged; independent of this MR
  • #627892 — removing import '~/commons' from the performance bar entrypoint; the same pattern is used here
  • &6252 — parent epic
Edited by Miguel Rincon

Merge request reports

Loading
Loading