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: theduo_panelentry, andduo_panelinALWAYS_LOADED_ENTRY_POINTS.spec/frontend/config/entry_points_spec.jsrequires 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 wayperformance_baris 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-prodbuild on the rebased branch: exits 0.duo_panel.<hash>.chunk.jsis emitted (570,838 bytes) andmain.<hash>.chunk.jsshrinks from 1,173,629 to 454,114 bytes, a 61% reduction.manifest.rspack.jsonlistsduo_panelwith the same shared chunks as the page entries plus its own, so on a page that renders the panelwebpack_entrypoint_pathsadds 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:
![]() |
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.ymlwithstatus: rolloutand avue3_migrate_duo_panelflag. 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 inconfig/helpers/entry_points.js, support inconfig/helpers/vite_plugin_page_entrypoints.mjsfor a global entry outside the CEentrypoints/directory, and anee/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 themain_ee.jsprecedent. entrypoints/duo_panel.jsimports~/commons, likesuper_sidebarandperformance_bar. Under a?vue3rollout that import duplicatescommons/*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
?vue3prototype 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
