perf(bundle): lazy-load emoji-mart picker (defer ~82 KB gzip from first paint)

@emoji-mart/data (~430 KB / ~82 KB gzip) was pulled into the initial load by three always-mounted views (catalog custom-prefix, nick-icon, chat-input emoji selector) importing it statically. Add LazyEmojiPicker — a Suspense wrapper that dynamically imports the data + Picker on first open — and use it in those three sites.

Verified via the build manifest: the entry chunk no longer imports vendor-emoji statically or dynamically; vendor-emoji is now a dynamicImport of src/index.tsx, loaded on first picker open. No behaviour change. typecheck 0, tests 545/545.
This commit is contained in:
simoleo89
2026-06-17 21:26:55 +02:00
parent 1b032bcd23
commit 4d81fe7c82
4 changed files with 38 additions and 11 deletions
+32
View File
@@ -0,0 +1,32 @@
import { ComponentType, FC, lazy, Suspense } from 'react';
type EmojiPickerProps = Record<string, unknown>;
/**
* emoji-mart's data bundle (`@emoji-mart/data`) is ~430 KB (~82 KB gzip) and was
* pulled into the initial app bundle by three always-mounted views that import it
* statically. The picker itself opens rarely, so we load both the data and the
* `<Picker>` component on demand via a dynamic import — deferring that payload out
* of first paint. Drop-in for `<Picker data={data} … />` (the `data` prop is
* injected here; forward every other prop unchanged).
*/
const PickerWithData = lazy(async () =>
{
const [ dataModule, pickerModule ] = await Promise.all([
import('@emoji-mart/data'),
import('@emoji-mart/react')
]);
const data = (dataModule as { default: unknown }).default;
const Picker = (pickerModule as { default: ComponentType<EmojiPickerProps> }).default;
const Wrapped: ComponentType<EmojiPickerProps> = props => <Picker data={ data } { ...props } />;
return { default: Wrapped };
});
export const LazyEmojiPicker: FC<EmojiPickerProps> = props => (
<Suspense fallback={ <div className="px-2 py-1 text-[11px] text-white/60"></div> }>
<PickerWithData { ...props } />
</Suspense>
);