Skip to content

⚡ Lazy-load heavy components on FE, on-demand locales, and per-font stylesheets - #1193

Merged
aaronleopold merged 2 commits into
stumpapp:nightlyfrom
balazs-szucs:chunk-tsconfig
Sep 17, 2026
Merged

aaronleopold merged 2 commits into
stumpapp:nightlyfrom
balazs-szucs:chunk-tsconfig

Conversation

@balazs-szucs

@balazs-szucs balazs-szucs commented May 30, 2026 •

Copy link
Copy Markdown
Contributor

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-mart dataset + renderer, and a markdown parser.

Changes are as follows:

  • MarkdownPreview and @emoji-mart/react are code-split via React.lazy(); emoji data fetch gated on isOpen
  • 32 locale JSONs replaced with import.meta.glob; only en-US is eager, rest load via loadLocaleResources
  • resolveLocale normalizes raw browser strings (fr, zh_CN) to a valid AllowedLocale, replacing a silent bad cast (At least I think so? I was getting weird errors)
  • EPUB reader injects only the selected font's stylesheet instead of a monolithic fonts.css

As for ResizeObserver added guard against post-teardown resize calls, it was giving also some headaches, not quite certain if I caused it somehow but 🤷‍♂️

@balazs-szucs
balazs-szucs marked this pull request as draft May 30, 2026 21:24
@balazs-szucs

balazs-szucs commented May 30, 2026 •

Copy link
Copy Markdown
Contributor Author

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.

@balazs-szucs

Copy link
Copy Markdown
Contributor Author

So, what tripped me up the maths vs my older precompress PR, so did some digging:

State Old PR This PR Difference
Uncompressed (Main bundle) 5.16 MB (~5,283 kB) 2.31 MB (2,309.48 kB) ~56%
Gzipped (Main bundle) 806 kB 713 kB ~11.5%

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.

@balazs-szucs
balazs-szucs marked this pull request as ready for review May 31, 2026 11:49
@balazs-szucs

balazs-szucs commented May 31, 2026 •

Copy link
Copy Markdown
Contributor Author

../dist/assets/hina-mincho-japanese-400-normal-CLxaJXTw.woff2 1,424.40 kB

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 aaronleopold left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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).

Comment thread packages/i18n/src/config.ts Outdated
Comment on lines +16 to +19
const localeLoaders = import.meta.glob<{ default: Translation }>([
'./locales/*.json',
'!./locales/en-US.json',
])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread packages/i18n/src/config.ts Outdated
Comment thread packages/i18n/src/config.ts Outdated
Comment thread packages/components/src/emoji/EmojiPicker.tsx
Comment thread packages/browser/src/components/readers/epub/EpubJsReader.tsx Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same remarks here as with the markdown preview

Comment thread apps/web/vite.config.ts Outdated
Comment thread apps/web/vite.config.ts Outdated
Comment thread apps/web/vite.config.mts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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'
      }
    },
  },
}

@balazs-szucs

balazs-szucs commented May 31, 2026 •

Copy link
Copy Markdown
Contributor Author

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).

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.

@aaronleopold aaronleopold left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think only one of my comments went missed, but otherwise I think this looks good 🙂

Comment thread packages/i18n/src/LocaleProvider.tsx Outdated
Comment on lines +18 to +35
let active = true
async function prepare() {
try {
await loadLocaleResources(resolvedLocale)
if (!active) {
return
}

await Promise.all([i18n.changeLanguage(resolvedLocale), initDateFnsLocale(resolvedLocale)])
document.documentElement.lang = resolvedLocale
} catch (error) {
console.error('Failed to load locale resources', error)
}
}
prepare()
return () => {
active = false
}

@aaronleopold aaronleopold Jun 4, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this bit still needs to be addressed, and also will change a bit with updates from nightly which fixed things for expo

@aaronleopold

Copy link
Copy Markdown
Collaborator

Hey @balazs-szucs, coming back to this and seeing it has gone a bit stale. Is this an effort you would like to revive? Or should I go ahead and close this one out?

@balazs-szucs

Copy link
Copy Markdown
Contributor Author

Hey @balazs-szucs, coming back to this and seeing it has gone a bit stale. Is this an effort you would like to revive? Or should I go ahead and close this one out?

I can fix, no worries. Apologies for not getting to this. Burnout and all. Obviously, no pressure eitherway.

@balazs-szucs

Copy link
Copy Markdown
Contributor Author

Okey, i think some changes upstream make stuff here bit obselete (e.g., reader changes). Still, i think majority of the stuff here is mostly unaffected, so no big worries here. Most of the conflicts look easy enough to resolve, so yeah all good.

@aaronleopold

Copy link
Copy Markdown
Collaborator

Apologies for not getting to this

Never any problems in this regard on my side, life is always first you're good

i think some changes upstream make stuff here bit obselete

That's okay too, if it's not worth reviving it for now that's totally okay by me

Manual vendor chunking, granular path imports, and query extraction for
smaller web bundles. Load i18n locale resources and date-fns locales on
demand, and lazy-load the emoji picker.
@balazs-szucs

balazs-szucs commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor Author

That's okay too, if it's not worth reviving it for now that's totally okay by me

I can quickly re-measure, i think. (Probably tmrw, as it's midnight)

But for me this looks good now 👍

I quickly retested/clicked around for a while it does looks good. Locale changes work, EPUB reader works, so no worries there i think.

Also 150 LOC leaner so that's also nice.

@aaronleopold

Copy link
Copy Markdown
Collaborator

Thank you! I'll resolve the conflicts and aim to get this merged in sometime during the week

@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 4.76190% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
apps/server/src/routers/spa.rs 4.76% 20 Missing ⚠️
Files with missing lines Coverage Δ
apps/server/src/routers/spa.rs 41.96% <4.76%> (-6.98%) ⬇️

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aaronleopold
aaronleopold merged commit 0676f3e into stumpapp:nightly Sep 17, 2026
9 checks passed
vint1024 added a commit to vint1024/NoirPanther that referenced this pull request Sep 20, 2026
91 upstream commits (v0.1.8 + v0.1.9), 79 conflicts.

Upstream fixed the four authorization holes we reported (A30, A32, A33) in
`:lock: Fix security audit findings`, with 12 new integration tests. Their
versions are taken and ours dropped: theirs also allow MANAGE_USERS holders to
administer other users and protect the server owner from being edited by them.
A31 (content rules must hide BOOKS) is fork-only code and stays, guarded by
`visibility_filters_are_applied`. Upstream's `cache_friendly_url` replaces our
versioned thumbnail URLs (A20).

Merged by hand where upstream restructured:
- core crate reorg (stumpapp#1424): our files moved with it — metadata (A26/A27/A29) to
  metadata/media.rs, EPUB collections (A28) and cover placeholder (A7) to
  media/processor + image/, writeback (A5) to metadata/provider; the three job
  files became job/dispatch.rs, where MetadataWriteback is re-registered.
- library scan: our multi-root walk (A1) and series-merge resolution (A2) now
  also thread upstream's oneshot discovery through every root.
- patch_library (new upstream mutation) validates all roots, not just the path.
- vite: upstream's chunking (stumpapp#1193) keeps react/router/i18next/date-fns and our
  i18n package in one chunk — split apart, rolldown builds a cyclic graph that
  breaks start-up (chunk_cycles.py reports 0 cycles).
- i18n: upstream localized several components themselves, so their keys win;
  en-US is theirs plus 732 fork-only keys, ru-RU re-keyed onto it (96 new
  Russian strings, 1 upstream-app key lost).
- tests: fork tables (content rules, library paths, series merges) are created
  in the in-memory test DB, without which the new upstream tests 500.

488 tests pass (415 unit + 73 server integration).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants