Skip to content

[EuiIcon] Rewrite icon assets to explicit dynamic imports#9342

Merged
weronikaolejniczak merged 5 commits intoelastic:mainfrom
weronikaolejniczak:feat/rewrite-icon-import
Feb 3, 2026
Merged

[EuiIcon] Rewrite icon assets to explicit dynamic imports#9342
weronikaolejniczak merged 5 commits intoelastic:mainfrom
weronikaolejniczak:feat/rewrite-icon-import

Conversation

@weronikaolejniczak
Copy link
Copy Markdown
Contributor

@weronikaolejniczak weronikaolejniczak commented Jan 29, 2026

Summary

  • Updates icon_map.ts to use dynamic imports instead of string values. 84b73d1
  • Use the updated icon_map.ts loaders directly in packages/eui/src/components/icon/icon.tsx. 84b73d1
  • Removed icon preloading in Storybook. a1bc973
  • Removed icon workaround section from wiki. e51ae23
  • Updated website supported environments section. 48b2168

Note

For consumers, please try removing your patches and appendIconComponentCache usages once the change gets released (ETA v112.3.0). If you find any issues, please open a new bug report.

Why are we making this change?

Resolves #5463
Resolves #5305
Relates to #8860

The gist of EUI icon approach

We hold all SVG files in: packages/eui/src/components/icon/svgs. We use svgr in packages/eui/scripts/compile-icons.js (that's executed in our building pipeline) to transform the SVG files into React components. We add .tsx files into packages/eui/src/components/icon/assets folder and in packages/eui/src/components/icon/icon_map.ts we reference the paths. Later, in EuiIcon source: packages/eui/src/components/icon/icon.tsx we reference this map:

import(
/* webpackChunkName: "icon.[request]" */
// It's important that we don't use a template string here, it
// stops webpack from building a dynamic require context.
// eslint-disable-next-line prefer-template
'./assets/' + typeToPathMap[iconType]
).then(({ icon }) => {

and access the assets/ folder paths using iconType passed to the EuiIcon component, importing the path dynamically and rendering the React components.

Problem context

For a long time, the way icons were loaded was by using one dynamic import with path string concatenation, as you can see above, and letting Webpack know to chunk it with magic comments. This is convenient because we can load them all with one import. Likely because Kibana uses Webpack and EUI was predominantly written for Kibana use.

The biggest downside to this approach is the vendor lock-in. As soon as modern bundlers started becoming standard, now tools like Vite, NextJS successfully burying CRA and Webpack-based tooling, this became an issue. In parallel, bundlers like Parcel started supporting dynamic imports. Modern bundlers need to see explicit dynamic imports to be able to load the modules.

Solution

The solution is very simple. Instead of writing path strings in icon_map.ts:

export const typeToPathMap = {
  accessibility: 'accessibility',
  // ...
}

we use dynamic imports:

export const typeToPathMap = {
  accessibility: () => import('./assets/accessibility'),
  // ...
}

that should allow all bundlers to statically see all needed modules, and chunk them accordingly.

Advantages

The bonus to this approach as opposed to appendIconComponentCache is:

  1. The icons just work no matter the bundler, no additional code needed on consumer side to make them work. This alleviates the preloaded icon list maintenance burden from consumers.
  2. The icons are loaded dynamically in runtime as they show up and not preloaded all at once. This improves the initial page loads. As icons appear, they are added to the cache and resolved from cache.

Disadvantages

The only downside of on-demand loading is a tiny delay when an icon appears but we have an in-built loading state (fade in) to prevent layout shifts. Once the icon is loaded, it is cached for the rest of the session.

Performance audits

Storybook (Webpack but no preloading)

Measured on the EuiButton Playground story.

Metric Before After Change
Total Blocking Time (TBT) 70 ms 40 ms -43% 🚀
JS Resources (Parsed) 12.8 MB 12.4 MB -453 KB
JS Transferred 2.40 MB 2.38 MB -26 KB
Speed Index 1.2s 1.2s -

Kibana (Webpack)

Followed Testing in Kibana wiki. Resolved @elastic/eui to package.tgz generated by running build-pack on this branch. Tested before and after.

Metric Before After Change
JS Requests 257 257 0
JS Transferred 16.41 MB 16.41 MB -
JS Resources 105.3 MB 105.3 MB -
Total Blocking Time (TBT) 1,120 ms 1,120 ms 0%

CLS was repeatedly lower by ~0.076 but it's fluctuating. Speed Index was reported 1s lower but I'm not confident it's saying much considering Kibana's size. What would warrant any worry would be the decrease in JS Requests (i.e. icons are possibly incorrectly chunked).

The bottom line is, there's no regression for the main consumer.

Screenshot 2026-02-02 at 14 32 45

Icons are chunked correctly. Since we removed the magic comments, the names are just hashes.

AutoOps (Vite/Rollup)

Followed AutoOps README.md. Resolved @elastic/eui to package.tgz generated by running build-pack on this branch. Tested before and after.

Metric Before After Change
JS Requests 279 230 -49 📉
JS Transferred 18.22 MB 18.07 MB -150 KB
JS Resources 19.43 MB 19.29 MB -140 KB
Total Blocking Time (TBT) 50 ms 50 ms 0%
Speed Index 6.4s 6.2s -0.2s

This effectively demonstrates the dynamically loaded icons (as opposed to preloaded ones).

Screenshot 2026-02-02 at 15 20 41

Additional details

#5463 (comment)

We had a brief sync about this today, and we'd be open to changing the import method.
One difficulty I see with the referenced path is that we have 4 different builds (es, lib, optimize/es, and optimize/lib) and the import path would need to change for each:

accessibility: () => import('@elastic/eui/es/components/icon/assets/accessibility')

Right now each build, as well as local dev (with webpack dev server), work because of the relative './assets/' pathing in the concatenated string. We'd need to alter the import at build time to accommodate each build but keep local imports working for dev environments.

This is simply not the case. The relative path dynamic imports work identically to the current string concatenation "./assets/" + type. When the project is built, no matter the target (es, lib, optimize/es, optimize/lib), the build preserves the directory structure. So the source icon_map.js is right next to assets/ folder. Because they move together to the destination folder, the relative path remains valid for all 4 build targets and local dev.

Missing extensions

All modern bundlers are capable of resolving the extension. There is no need to provide them directly in our source code. If we did that, it would have to be .js and that would lead to references not being resolved on our side and TypeScript overrides / muting comments.

Impact to users

🟢 Webpack-based projects keep resolving and chunking the icons as needed. Projects using modern bundlers like Rollup, Parcel don't require appendIconComponentCache usage and all icons are loaded runtime as used, not all preloaded.

QA

Specific checklist

  • Verify that Kibana CI is passing ([Test] EuiIcon rewrite kibana#251263)
  • Verify VRTs pass
  • Verify that icons can be used without appendIconComponentCache in a boilerplate Parcel project
    • yarn workspace @elastic/eui build-pack
    • npm create parcel react-client test-eui-parcel (see docs)
  • Verify that icons can be used without appendIconComponentCache in a boilerplate Vite project (Rollup)
    • yarn workspace @elastic/eui build-pack
    • npm create vite@latest test-eui-vite -- --template react-ts (see docs)
  • Verify that icons can be used without appendIconComponentCache in AutoOps project (thanks @dfvalero 🙌🏻)
  • Icons render as expected in Storybook staging and locally
  • Icons render as expected in the documentation website staging and locally
  • Run performance tests on Storybook
  • Run performance tests on AutoOps
  • Run performance tests on Kibana

I went through these myself, feel free to double-check.

General checklist

  • Browser QA
    • Checked in both light and dark modes
    • Checked in both MacOS and Windows high contrast modes
    • Checked in mobile
    • Checked in Chrome, Safari, Edge, and Firefox
    • Checked for accessibility including keyboard-only and screenreader modes
  • Docs site QA
  • Code quality checklist
  • Release checklist
    • A changelog entry exists and is marked appropriately
    • If applicable, added the breaking change issue label (and filled out the breaking change checklist)
    • If the changes unblock an issue in a different repo, smoke tested carefully (see Testing EUI features in Kibana ahead of time)
  • Designer checklist
    • If applicable, file an issue to update EUI's Figma library with any corresponding UI changes. (This is an internal repo, if you are external to Elastic, ask a maintainer to submit this request)

@weronikaolejniczak weronikaolejniczak changed the title feat(eui): rewrite icons to explicit dynamic imports [EuiIcon] Rewrite icons to explicit dynamic imports Jan 29, 2026
@weronikaolejniczak weronikaolejniczak changed the title [EuiIcon] Rewrite icons to explicit dynamic imports [EuiIcon] Rewrite icon assets to explicit dynamic imports Jan 29, 2026
@tkajtoch tkajtoch self-requested a review January 29, 2026 14:25
@weronikaolejniczak weronikaolejniczak self-assigned this Jan 30, 2026
@elasticmachine
Copy link
Copy Markdown
Collaborator

💚 Build Succeeded

History

cc @weronikaolejniczak

@elasticmachine
Copy link
Copy Markdown
Collaborator

elasticmachine commented Jan 30, 2026

💚 Build Succeeded

History

cc @weronikaolejniczak

@weronikaolejniczak weronikaolejniczak marked this pull request as ready for review February 2, 2026 14:38
@weronikaolejniczak weronikaolejniczak requested a review from a team as a code owner February 2, 2026 14:38
Copy link
Copy Markdown
Contributor

@dfvalero dfvalero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't express how happy I am to see this getting fixed. Really appreciate it!

Copy link
Copy Markdown
Member

@tkajtoch tkajtoch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested various usecases locally and can confirm the solution works as reliably as the previous one while bringing support for non-webpack bundlers. Code changes look good. LGTM

@weronikaolejniczak weronikaolejniczak merged commit 548ac3b into elastic:main Feb 3, 2026
6 checks passed
@adhishthite
Copy link
Copy Markdown

AWESOME ! How can integrate into Vite now?

@weronikaolejniczak
Copy link
Copy Markdown
Contributor Author

Hey @adhishthite 👋🏻 it should be released next week (likely v112.3.0). All you have to do is remove your appendIconComponentCache usage and all icons should load dynamically. LMK if it doesn't work!

@adhishthite
Copy link
Copy Markdown

adhishthite commented Feb 3, 2026 via email

@cee-chen
Copy link
Copy Markdown
Contributor

cee-chen commented Feb 3, 2026

Amazing job on this Weronika!!! 🥹 You guys are doing such great work!

@weronikaolejniczak weronikaolejniczak deleted the feat/rewrite-icon-import branch February 10, 2026 10:00
mgadewoll added a commit to elastic/kibana that referenced this pull request Feb 10, 2026
## Dependency updates

- `@elastic/eui`: v112.2.0 ⏩ v112.3.0
- `@elastic/eslint-plugin-eui`: v2.7.0 ⏩ v2.8.0

---

## Package updates

### `@elastic/eui`
[v112.3.0](https://github.com/elastic/eui/releases/tag/v112.3.0)

- Added new `server` icon.
([#9355](elastic/eui#9355))
- Added `className` support to `EuiMarkdownEditor`'s `toolbarProps` for
custom toolbar styling
([#9349](elastic/eui#9349))
- Updated `EuiFilePicker` to use the `upload` icon to better indicate
uploads. ([#9351](elastic/eui#9351))
- Exported the flyout system store singleton and added an event observer
for emitting close session events
([#9347](elastic/eui#9347))
- Updated `EuiIcon` to use standard dynamic imports for icon assets,
enabling native support for modern bundlers (Rollup, Parcel) and
improving initial load performance
([#9342](elastic/eui#9342))

**Bug fixes**

- Fixed a potential crash in the flyout system: due to asynchronous
state updates and React's batching behavior, it was possible to
experience a crash when closing a managed flyout.
([#9356](elastic/eui#9356))

### `@elastic/eslint-plugin-eui` v2.8.0

- Added new `icon-accessibility-rules` rule.
([#9357](elastic/eui#9357))
- Added new `badge-accessibility-rules` rule.
([#9354](elastic/eui#9354))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vite/Rollup/ESBuild cannot process the dynamic imports in EuiIcon ts(7016) error when trying to import icons

6 participants