Skip to content

fix: emit deep per-icon imports for static lucide icons#6628

Merged
masenf merged 7 commits into
mainfrom
khaleel/eng-9721-rxicon-floods-dev-server-with-1700-requests-via-lucide-react
Jun 9, 2026
Merged

fix: emit deep per-icon imports for static lucide icons#6628
masenf merged 7 commits into
mainfrom
khaleel/eng-9721-rxicon-floods-dev-server-with-1700-requests-via-lucide-react

Conversation

@adhami3310

Copy link
Copy Markdown
Member

Problem

Static-string rx.icon tags (e.g. rx.icon("wifi_off")) compile to a barrel import:

import { WifiOff } from "lucide-react"

On its own this is fine (Vite tree-shakes it). But once the module graph also contains lucide-react/dynamic.mjs — which rx.icon uses for dynamic / state-Var tags via DynamicIcon — esbuild's dep-optimizer emits a per-icon chunk for the dynamic-import map and rewrites the barrel to statically import every one of them. Since DefaultOverlayComponents (the connection banner, mounted on every page) barrel-imports WifiOff, loading any page in dev fired ~1700 requests — the whole icon library.

Confirmed empirically: barrel alone = 1 request, DynamicIcon alone = 1 request, both together = 1715.

Fixes #6627.

Fix

Emit deep per-icon default imports (lucide's recommended form) for the static path:

// from:
import { WifiOff } from "lucide-react"
// to:
import LucideWifiOff from "lucide-react/dist/esm/icons/wifi-off.mjs"
  • lucide-react ships no exports map, so the path targets the kebab-case file directly; the .mjs extension is required for the integration harness's Node/Bun ESM resolver (extensionless only resolves under Vite's prod bundler).
  • The dynamic VarDynamicIcon (lucide-react/dynamic.mjs) path is unchanged — it needs the runtime map and doesn't flood.
  • DefaultOverlayComponents / the connection banner (WifiOffPulse) goes through the same Icon codegen, so it's fixed automatically.
  • A 6-entry file-name override covers icons whose kebab name doesn't match lucide's file (fingerprintfingerprint-pattern, and the grid_2x_2* / grid_3x_3 digit-x-digit names), verified exhaustively against the full 1721-icon list.

Testing

  • New unit tests: static icon emits the deep default import (not the barrel), per-icon parametrized path check, dynamic icon still uses dynamic.mjs, idempotency across cache hits.
  • tests/integration/test_icon.py passes in dev + prod.
  • Full tests/units/components + tests/units/compiler suites pass; ruff + pyright clean; .pyi stubs regenerated.

Static-string `rx.icon` tags compiled to a barrel import
`import { WifiOff } from "lucide-react"`. Once the module graph also
contains `lucide-react/dynamic.mjs` (used by DynamicIcon for Var tags),
esbuild's dep-optimizer emits a per-icon chunk for the dynamic-import map
and rewrites the barrel to statically import every one of them. Since
DefaultOverlayComponents (mounted on every page) barrel-imports WifiOff,
loading any page in dev fired ~1700 requests — the whole icon library.

Emit deep default imports instead, lucide's recommended form:
`import LucideWifiOff from "lucide-react/dist/esm/icons/wifi-off.mjs"`.
lucide-react has no exports map, so the path targets the kebab-case file
directly; the .mjs extension is required for Node/Bun ESM resolution.
The dynamic Var -> DynamicIcon path is left unchanged.

A 6-entry file-name override covers the icons whose kebab name doesn't
match lucide's file (fingerprint, grid_2x_2*, grid_3x_3), verified
against the full icon list.

Fixes #6627
ENG-9721
@adhami3310 adhami3310 requested a review from a team as a code owner June 8, 2026 20:59
@linear-code

linear-code Bot commented Jun 8, 2026

Copy link
Copy Markdown

ENG-9721

@codspeed-hq

codspeed-hq Bot commented Jun 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing khaleel/eng-9721-rxicon-floods-dev-server-with-1700-requests-via-lucide-react (f9641ec) with main (31f785e)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

masenf
masenf previously approved these changes Jun 8, 2026
WifiOffPulse extends Icon, so it inherits the new _import_path field —
regenerate its .pyi hash, which I missed.

Move the news fragment from news/6627 (issue number, root) to
packages/reflex-components-lucide/news/6628 (PR number, affected
package) so the per-package towncrier check finds it.
@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the ~1700 dev-mode requests-per-page-load regression caused by esbuild's dep-optimizer expanding the lucide-react barrel import to all ~1715 icon chunks whenever lucide-react/dynamic.mjs was also in the module graph. The fix replaces the barrel { WifiOff } named import with a deep per-icon default import (import LucideWifiOff from "lucide-react/dist/esm/icons/wifi-off.mjs"), and adds a companion pyi_generator fix to stop private underscore-prefixed fields from leaking into generated stubs.

  • Icon._get_imports() overrides the barrel-import path by grouping each icon's ImportVar under its own package_path; compile_imports in reflex/compiler/utils.py already groups by package_path to emit one import statement per unique subpath, so multiple icons on a page each get an independent deep import correctly.
  • LUCIDE_ICON_FILENAME_OVERRIDE covers the 6 icons whose kebab-case name diverges from lucide's actual file path (fingerprint, grid_2x_2*, grid_3x_3); DynamicIcon and its dynamic.mjs path are untouched.
  • pyi_generator.py adds name.startswith("_") to the skip-list so _import_path (and any future internal fields) are excluded from .pyi stubs.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to import path rewriting, the compiler already handles per-subpath grouping, and the dynamic icon path is untouched.

The core change rewrites a single well-understood mechanism (barrel import to deep per-icon default import) that already has an established pattern in DynamicIcon. The compiler's compile_imports groups ImportVar entries by package_path, so multiple icons on a page each receive their own import statement without conflict. The 6-entry LUCIDE_ICON_FILENAME_OVERRIDE is verified against the full icon list and spot-checked with independent literal assertions in the test suite.

No files require special attention.

Important Files Changed

Filename Overview
packages/reflex-components-lucide/src/reflex_components_lucide/icon.py Core fix: adds _import_path private field, overrides _get_imports() to emit deep per-icon default imports instead of barrel, and adds LUCIDE_ICON_FILENAME_OVERRIDE for 6 kebab-mismatch cases. Logic is correct and mirrors DynamicIcon's existing package_path pattern.
packages/reflex-base/src/reflex_base/utils/pyi_generator.py One-line addition: skips underscore-prefixed fields when emitting .pyi stubs, preventing internal fields like _import_path from leaking into the public API surface.
tests/units/components/lucide/test_icon.py Comprehensive new tests covering: deep import path on static icons, parametrized path check for all 1721 icons, literal expected paths for all 6 override entries, dynamic icon still uses dynamic.mjs, and idempotency across repeated calls.
tests/units/reflex_base/utils/pyi_generator/golden/classvar_and_private.pyi Golden file updated to remove _internal_counter from the generated stub, confirming the pyi_generator change behaves correctly for underscore-prefixed fields.
pyi_hashes.json Hash updated for icon.pyi to reflect regenerated stub after _import_path was excluded from the public API.

Reviews (2): Last reviewed commit: "add news for pyi_generator change" | Re-trigger Greptile

Comment thread packages/reflex-components-lucide/src/reflex_components_lucide/icon.py Outdated
Comment thread tests/units/components/lucide/test_icon.py
adhami3310 and others added 4 commits June 8, 2026 14:13
AccordionIcon(Icon) inherits the new _import_path field, so its
generated .pyi hash changed too. This was the remaining drift causing
pre-commit's update-pyi-files hook to fail. AccordionIcon and
WifiOffPulse are the only two Icon subclasses; both stubs are now
regenerated.
Address Greptile review on #6628:
- test_static_icon_import_path was circular for override entries (built
  the expected path from the same LUCIDE_ICON_FILENAME_OVERRIDE the code
  reads). Add test_static_icon_import_path_literal with hardcoded paths,
  independent of the override dict, so a typo in an override value fails.
- Complete the dangling 'doesn't' clause in the override-map comment.
These are "private" and should not form part of the IDE/type hinting
documentation. They are still accepted and won't raise any typing errors due to
all components _also_ taking `**props`
@masenf masenf merged commit 6f5c80b into main Jun 9, 2026
108 checks passed
@masenf masenf deleted the khaleel/eng-9721-rxicon-floods-dev-server-with-1700-requests-via-lucide-react branch June 9, 2026 01:19
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.

rx.icon static icons emit a lucide-react barrel import, flooding the dev server with ~1700 requests

2 participants