The main chunk carried everything the mail view might open, whether or not it ever did. A source-map breakdown of index-*.js showed these avoidable pieces (minified sizes):
the file preview with marked (~49 KB);
the composer, rich-text editor and pickers (~30 KB);
ContactEditor (14 KB);
the calendar, contacts and files sidebars (~22 KB);
the filter and share dialogs, plus the Sieve code they bring (~24 KB).
Each is now a React.lazy component inside a Suspense boundary with no fallback:
Component
Where it loads
FilePreviewDialog
MessageView, and only mounted while a preview is open. The Files view already loaded lazily, so it is unchanged.
FilterFromMessageDialog
MessageView and MessageList
ContactEditor
AddressMenu
ShareDialog
MailboxTree
CalendarSidebar, ContactsSidebar, FilesTree
AppShell, loaded with their sections, whose views were already lazy
Composer
ComposerDock
The composer is also fetched ahead of time. It's requested with requestIdleCallback after startup, falling back to a 2-second timeout, so the first Compose doesn't wait on the network.
notify.ts is imported statically now. It was imported both statically and dynamically, so the dynamic imports split nothing, and Vite warned about it (INEFFECTIVE_DYNAMIC_IMPORT). It only imports basePath, so importing it statically can't create an import cycle.
main
This branch
index-*.js
598 KB (183 KB compressed)
460 KB (145 KB compressed)
Everything index.html loads, gzip -9
273 KB
236 KB (−14%)
Both build warnings are gone: the ineffective dynamic import, and the chunk over 500 kB.
Related issues
None.
Translations
Adds none. npm run i18n:check reports the same counts as main.
Testing
npm test -w web passes (1,320 tests). composer-dock.test.tsx now waits for the lazy composer to arrive before checking the dock. tsc and vite build are clean.
Checked in Chrome against the production build served on port 8080 by npm run dev:mock. Resource timing confirmed that each chunk arrived only when needed:
The composer was fetched while idle, before any click, and opened with its full editor.
The file preview wasn't loaded at startup. Clicking an image attachment fetched it, and the dialog showed the image.
"Edit contact" from a sender's right-click menu fetched ContactEditor and opened the editor with its fields.
"Filter messages like this…" from a row's context menu fetched FilterFromMessage and opened the dialog.
The calendar, contacts and files sidebars weren't loaded at startup; each loaded with its section and rendered its content.
Not checked in the browser: the share dialog from the mail folder tree. Its only entry point there is "Stop sharing" on a folder that is already shared, and the mock inbox has none. It's wired the same way as the five above.
## Summary
The main chunk carried everything the mail view might open, whether or not it ever did. A source-map breakdown of `index-*.js` showed these avoidable pieces (minified sizes):
- the file preview with `marked` (~49 KB);
- the composer, rich-text editor and pickers (~30 KB);
- `ContactEditor` (14 KB);
- the calendar, contacts and files sidebars (~22 KB);
- the filter and share dialogs, plus the Sieve code they bring (~24 KB).
Each is now a `React.lazy` component inside a `Suspense` boundary with no fallback:
| Component | Where it loads |
|---|---|
| `FilePreviewDialog` | `MessageView`, and only mounted while a preview is open. The Files view already loaded lazily, so it is unchanged. |
| `FilterFromMessageDialog` | `MessageView` and `MessageList` |
| `ContactEditor` | `AddressMenu` |
| `ShareDialog` | `MailboxTree` |
| `CalendarSidebar`, `ContactsSidebar`, `FilesTree` | `AppShell`, loaded with their sections, whose views were already lazy |
| `Composer` | `ComposerDock` |
**The composer is also fetched ahead of time.** It's requested with `requestIdleCallback` after startup, falling back to a 2-second timeout, so the first Compose doesn't wait on the network.
**`notify.ts` is imported statically now.** It was imported both statically and dynamically, so the dynamic imports split nothing, and Vite warned about it (`INEFFECTIVE_DYNAMIC_IMPORT`). It only imports `basePath`, so importing it statically can't create an import cycle.
| | `main` | This branch |
|---|---|---|
| `index-*.js` | 598 KB (183 KB compressed) | **460 KB (145 KB compressed)** |
| Everything `index.html` loads, gzip -9 | 273 KB | **236 KB (−14%)** |
Both build warnings are gone: the ineffective dynamic import, and the chunk over 500 kB.
## Related issues
None.
## Translations
Adds none. `npm run i18n:check` reports the same counts as `main`.
## Testing
- `npm test -w web` passes (1,320 tests). `composer-dock.test.tsx` now waits for the lazy composer to arrive before checking the dock. `tsc` and `vite build` are clean.
- Checked in Chrome against the production build served on port 8080 by `npm run dev:mock`. Resource timing confirmed that each chunk arrived only when needed:
- The composer was fetched while idle, before any click, and opened with its full editor.
- The file preview wasn't loaded at startup. Clicking an image attachment fetched it, and the dialog showed the image.
- "Edit contact" from a sender's right-click menu fetched `ContactEditor` and opened the editor with its fields.
- "Filter messages like this…" from a row's context menu fetched `FilterFromMessage` and opened the dialog.
- The calendar, contacts and files sidebars weren't loaded at startup; each loaded with its section and rendered its content.
- **Not checked in the browser:** the share dialog from the mail folder tree. Its only entry point there is "Stop sharing" on a folder that is already shared, and the mock inbox has none. It's wired the same way as the five above.
**Merged** 2026-09-16 as coffey-labs/ihasmail@9560ad06f435
<sub>Rebuilt from: GH Archive, git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
The main chunk carried everything the mail view might open, whether or not it ever did. A source-map breakdown of
index-*.jsshowed these avoidable pieces (minified sizes):marked(~49 KB);ContactEditor(14 KB);Each is now a
React.lazycomponent inside aSuspenseboundary with no fallback:FilePreviewDialogMessageView, and only mounted while a preview is open. The Files view already loaded lazily, so it is unchanged.FilterFromMessageDialogMessageViewandMessageListContactEditorAddressMenuShareDialogMailboxTreeCalendarSidebar,ContactsSidebar,FilesTreeAppShell, loaded with their sections, whose views were already lazyComposerComposerDockThe composer is also fetched ahead of time. It's requested with
requestIdleCallbackafter startup, falling back to a 2-second timeout, so the first Compose doesn't wait on the network.notify.tsis imported statically now. It was imported both statically and dynamically, so the dynamic imports split nothing, and Vite warned about it (INEFFECTIVE_DYNAMIC_IMPORT). It only importsbasePath, so importing it statically can't create an import cycle.mainindex-*.jsindex.htmlloads, gzip -9Both build warnings are gone: the ineffective dynamic import, and the chunk over 500 kB.
Related issues
None.
Translations
Adds none.
npm run i18n:checkreports the same counts asmain.Testing
npm test -w webpasses (1,320 tests).composer-dock.test.tsxnow waits for the lazy composer to arrive before checking the dock.tscandvite buildare clean.npm run dev:mock. Resource timing confirmed that each chunk arrived only when needed:ContactEditorand opened the editor with its fields.FilterFromMessageand opened the dialog.Merged 2026-09-16 as coffey-labs/ihasmail@9560ad06f4
Rebuilt from: GH Archive, git history, session transcript.