⚡ Lazy-load heavy components on FE, on-demand locales, and per-font stylesheets#1193
⚡ Lazy-load heavy components on FE, on-demand locales, and per-font stylesheets#1193balazs-szucs wants to merge 15 commits into
Conversation
# Conflicts: # apps/web/vite.config.ts
|
I made this a draft, as I think, something stinks here. I'll take second/third look at this. May be nothing. Just a gut feeling. |
|
So, what tripped me up the maths vs my older precompress PR, so did some digging:
So, yeah this is more like 12% percent improvement not 50%. Still, good? I think? I guess compression does not scale as the non-compressed file goes, so lot of resource that were cut off are exactly the stuff that were well compressed, hence why does does not bigger gains. So, in other words: lazy loading them shrinks the raw file dramatically, but their contribution to the compressed size was already minimal. GZIP "hid" the cost of those resources before, so deffering them doesn't move the needle as much post-compression. Probably locales files, i think, that contained huge amount formatting/whitespaces etc, so those are very well compressed. (best guess) Apologies for the confusion. |
That seems like a very hefty thing to be pulling in. There should be a more lightweight option, i think. (I am defo not suggesting dropping it, rather finding more suitable way to get the fonts.) |
aaronleopold
left a comment
There was a problem hiding this comment.
Thanks for working through this! I had some comments throughout.
So, yeah this is more like 12% percent improvement not 50%. Still, good? I think?
Yeah I think 12% is a good improvement, but based on what I saw I am worried that the improvement might be more artificial than practical (e.g., waterfall navigation from lazy loading routers).
| const localeLoaders = import.meta.glob<{ default: Translation }>([ | ||
| './locales/*.json', | ||
| '!./locales/en-US.json', | ||
| ]) |
There was a problem hiding this comment.
This will likely break expo. import.meta.glob is a vite API, right? Not all apps in the repo are using vite, so if we want to lazily load things we will need another mechanism for doing so
There was a problem hiding this comment.
I'll double check, I am afraid I did not think to test with expo. So whether this is breaking for it, i would need check.
Apologies.
| resources, | ||
| }) | ||
|
|
||
| export const resolveLocale = (inputLocale?: string): AllowedLocale => { |
There was a problem hiding this comment.
I'm a little conflicted about this function, at least for some of it. I think it might be overly defensive, really. I'm of the mentality that breaking fast will be easier to catch, e.g. if someone's navigator returns a locale which we don't support I'd rather bake in support for it than not realize it isn't working as expected.
There was a problem hiding this comment.
That may be, i think this is fine? I think the fallback to en-US is fine, most of (if any) problems are logged well enough, at the end of the day, I do not think this should cause any harm.
On the client, I think we would want to avoid throwing/failing on principle.
For the pt, i think you are right, i'll push commit when I am at PC again.
| const languageMatch = LOCALES.find((locale) => | ||
| locale.toLowerCase().startsWith(`${languageCode}-`), | ||
| ) |
There was a problem hiding this comment.
There are locales which could be matched here, but we'd only ever return the first. A bit of a guessing game (e.g. if my navigator passed just pt it would flow thorugh here to pt-BR when I might actually want pt-PT).
| const normalizedInput = inputLocale.replace('_', '-').toLowerCase() | ||
| const exactMatch = LOCALES.find((locale) => locale.toLowerCase() === normalizedInput) | ||
| if (exactMatch) { | ||
| return exactMatch | ||
| } |
There was a problem hiding this comment.
This feels redundant, did you observe a locale which came through in this format?
| } | ||
|
|
||
| if (!data) { | ||
| if (isOpen && !data) { |
There was a problem hiding this comment.
I don't see much value here, if you are on a route which has an emoji picker then IMO it should fetch the emojis before you open it. I think the provides a better UX, but maybe I am missing something.
Also, though, long term we won't be fetching emojis in this way so I don't feel too strongly.
There was a problem hiding this comment.
Also, though, long term we won't be fetching emojis in this way so I don't feel too strongly.
That would a huge update, (in terms of UX) but I am afraid I did take a look at this already, and emoji land seems to be a... Bit more complicated than I had realised, hence I think updates to this is fine. (It is probably bulky change when/if somebody gets to this.). I am not attached to this particular change.
| const renditionWithManager = rendition as Rendition & { | ||
| manager?: { stage?: unknown } | ||
| } | ||
| if (!renditionWithManager.manager?.stage) { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Curious about this one, was this something observed/corrected during runtime? I won't nitpick epubjs changes too much because I am eager to just eventually replace it with readium, but curiosity won 😄
There was a problem hiding this comment.
Same remarks here as with the markdown preview
| if (id.includes('lucide-react')) { | ||
| return 'vendor-lucide' | ||
| } |
There was a problem hiding this comment.
We don't reach this because we would have matched id.includes('react') above, right?
| output: { | ||
| manualChunks(id) { | ||
| if (id.includes('node_modules')) { | ||
| if (id.includes('react') || id.includes('scheduler')) { |
There was a problem hiding this comment.
Many packages will include react in the name, right?
There was a problem hiding this comment.
In practice, they are not that big :)
So, i think it should be just fine.
There was a problem hiding this comment.
I think my point was really in the same vein as this above, that it could falsely match packages (like lucide-react)
There was a problem hiding this comment.
I don't disagree with manual chunking in general, but I think maybe things might be a bit higher fidelity if we compare more complete paths instead of just substrings. E.g.
rollupOptions: {
output: {
manualChunks(id) {
const path = id.replaceAll('\\', '/')
if (!path.includes('/node_modules/')) return
if (path.includes('/node_modules/lucide-react/')) return 'vendor-lucide'
if (path.includes('/node_modules/@tanstack/')) return 'vendor-tanstack'
// ... etc ...
if (
path.includes('/node_modules/react/') ||
path.includes('/node_modules/react-dom/') ||
path.includes('/node_modules/scheduler/')
) {
return 'vendor-react'
}
},
},
}
I redid the benchmark now its 626.23 kB so -22.3%, I think some of more commits after I made my initial comment improved it significantly. In practice, I feel like this should be an improvement, BUT a "hard to measure" improvement though. Lot of the chunking improvement will manifest themselves with caching/etc improvement, that is not necessarily easy to get a good measurement on. Anyways, last 8 commit, I think addressed everything. |
Cuts initial JS payload by ~50% (4050 kB to 2039 kB gzipped).
The app was eagerly loading a lot of weight that most users never need on boot, e.g., 32 locale JSON files, the full
@emoji-martdataset + renderer, and a markdown parser.Changes are as follows:
MarkdownPreviewand@emoji-mart/reactare code-split viaReact.lazy(); emoji data fetch gated onisOpenimport.meta.glob; onlyen-USis eager, rest load vialoadLocaleResourcesresolveLocalenormalizes raw browser strings (fr,zh_CN) to a validAllowedLocale, replacing a silent bad cast (At least I think so? I was getting weird errors)fonts.cssAs for
ResizeObserveradded guard against post-teardown resize calls, it was giving also some headaches, not quite certain if I caused it somehow but 🤷♂️