make mockup pattern only load the heaving parts when needed
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 57
- Forks
- 103
- Avg merge
- 10h 25m
- Merged PRs (30d)
- 18
Description
The problem
Every Classic UI page ships and evaluates the same mockup JavaScript
before anything on the page is interactive — measured at ~827 KB over
9 requests on a logged-in page (~771 KB anonymous; see
Measured effect). A large share of that is code for widgets
that only ever appear on specific add/edit forms (recurrence, querystring,
related items, upload, select2), plus infrastructure that is dragged into the
eager bundle even though it's rarely used:
- the whole Svelte runtime (~110 KB), pulled in by a top-level
import { mount }in the contentbrowser/filemanager registration modules; - a bootstrap-icons
import()context covering all 2,078 icons
(~125 KB of filename→chunk maps in the eager bundle, plus ~2,000 tiny
generated chunk files); - a formatjs Intl polyfill + locale context over ~766 locale files
(~35 KB of maps, plus one generated chunk per locale), loaded even though
every supported browser shipsIntl.DateTimeFormatnatively.
All of this is downloaded, parsed and executed on every page navigation,
whether or not the relevant element exists in the DOM — wasted bandwidth,
wasted main-thread CPU, and needless cache churn on every deploy.
What changed
Each change follows the same, established patternslib convention: split a
thin registration module (eager, just the trigger + options) from the heavy
implementation (lazy, imported only when a matching element exists). No
public option or markup contract changes — widgets behave identically.
| PR / branch | Change | Removed from the every-page bundle |
|---|---|---|
eager-weight |
Move svelte (mount/unmount) + @plone/registry imports into the init() dynamic-import path |
~110 KB min |
eager-weight |
Resolve bootstrap icons via fetch() of the existing static-resource URL instead of a webpack import() context |
~125 KB min + ~2,000 generated chunk files |
eager-weight |
Feature-detect / lazy-load the formatjs Intl polyfill instead of forcing it eagerly | ~35 KB min + ~766 generated chunk files |
mockup-lazy-recurrence |
Lazify pat-recurrence (incl. 3 inlined XML templates + Modal) | 52 KB src → on-demand |
mockup-lazy-querystring |
Lazify pat-querystring (also removes an eager edge into contentbrowser) | 33 KB src → on-demand |
mockup-lazy-relateditems |
Lazify pat-relateditems | 27 KB src → on-demand |
mockup-lazy-upload |
Lazify pat-upload (lazy Dropzone) | 17 KB src → on-demand |
mockup-lazy-select2 |
Lazify pat-select2 + its locale context | 13 KB src → on-demand |
The lazified patterns are still registered eagerly (so their trigger keeps
working); only their implementation code moves into a chunk that loads the
first time a matching element is on the page.
Measured effect
Every-page runtime payload
Measured with Playwright (empty cache, networkidle) against a running Plone
site, loading the site root and counting only the mockup requests/bytes
(everything under ++plone++static/bundle-plone/). Each build was compiled into
plone.staticresources, the Zope restarted, and the site loaded in turn.
Logged-in (admin), clean before/after:
| Mockup | Requests | Payload (uncompressed) |
|---|---|---|
| baseline | 9 | 827 KB |
| optimized | 8 | 462 KB |
| change | −1 | −365 KB (−44%) |
Anonymous, same before/after:
| Mockup | Requests | Payload (uncompressed) |
|---|---|---|
| baseline | 8 | 771 KB |
| optimized | 7 | 402 KB |
| change | −1 | −369 KB (−48%) |
Notes: the baseline
bundle.min.jsserved (295,207 B) matched the clean
masterbuild to the byte, and the eager svelte runtime chunk (~172 KB,
chunk68551) is present on baseline and gone on optimized — confirming the
two builds. An earlier one-off run of a fully-combined working tree reported
787 KB → 482 KB (−305 KB); the measured numbers above supersede it.
Build artifacts (reproduced)
Production build of each branch, NODE_ENV=production (default dist/
target), counting the emitted files:
| Build metric | master (baseline) | optimized¹ | change |
|---|---|---|---|
Total files under dist/ (js + maps + assets) |
5,075 | 1,469 | −3,606 (−71%) |
| JS chunk files | 3,576 | 734 | −2,842 (−79%) |
bundle.min.js (uncompressed) |
288 KB | 208 KB | −80 KB (−28%) |
¹ Optimized = eager-weight (the three infra fixes). The bulk of the
file-count drop is the removed bootstrap-icons + Intl locale contexts. The
five widget-lazification
branches move their implementation into on-demand chunks, further trimming the
every-page runtime payload above without materially changing the total
file count.
What we gain
- ~365 KB less JavaScript (~44% of the every-page payload) downloaded and
evaluated on ordinary page views (measured, logged-in; ~48% anonymous). The
widget code now loads only on the add/edit forms that actually use it. - Less main-thread CPU on every navigation — the biggest cost is not the
download (HTTP caches that) but parsing and evaluating the code on each
page load. Cutting the eager closure cuts time-to-interactive on every
Classic UI page. - Far fewer generated files shipped into
plone.staticresources
(icons + Intl locale contexts removed), shrinking the static-resources
directory and deploy artifacts dramatically. Measured (production build):
total emitted files drop from 5,075 to 1,469 (−71%); of those, JS chunk
files drop from 3,576 to 734 (−2,842 fewer, −79%). - Better cache efficiency — a smaller, more stable eager bundle means
fewer bytes to re-fetch after each deploy busts the resource cache. - No behaviour or API change — pure load-time optimization; every widget
keeps its options, markup, and runtime behaviour.
Notes
- These are mostly independent PRs and can land in any order. One exception is the #1617 which has to be merged after #1614 was merged!
The relateditems / select2 pair is coordinated so neither drags the other back into the eager
bundle. - The three infra fixes (svelte, icons, Intl) currently share one working
branch,eager-weight, which also carries unrelated work. Split the infra
fixes out from that work before opening the PR(s). Note there is also a
separatemockup-lazy-integrationbranch that combines the five widget
lazifications with a different, more aggressive Intl change (it removes
core/intl-loader.jsoutright); no single committed branch currently equals
the fully-combined optimized state (infra fixes + all five widget
lazifications). - Production build of mockup needs
NODE_OPTIONS=--max-old-space-size=8192
(webpack OOMs at the default heap) — unrelated to these changes but worth a
reviewer note. - Follow-ups identified but out of scope here (they live outside
src/mockup):moment.jsstill loads on every logged-in page via
patternslibpat-display-time(toolbar history timestamp); and the
patternslib bootstrap evaluates a couple of lazy chunks in two waves on
every page regardless of triggers. Both tracked separately.
Verification
Fact-checked on 2026-07-14 against the mockup checkout:
- Reproduced by clean rebuild (worktree,
NODE_ENV=production): the
build-artifact table —master5,075 files / 3,576 JS chunks / 288 KB
bundle.min.jsvseager-weight1,469 / 734 / 208 KB. - Confirmed from source: widget sizes (recurrence.js 52 KB, querystring.js
33 KB, relateditems.js 27 KB, upload.js 17 KB, select2.js 13 KB); 2,078
bootstrap-icons; 766 formatjsintl-datetimeformatlocale files; the
svelte/icons/Intl removals match the commit messages on
eager-weight; select2 ↔ relateditems share commit
406a25742;pat-display-timedoesawait import("moment"). - Re-measured live against a running Plone site, each build compiled into
plone.staticresourcesand the Zope restarted (Playwright,bundle-plone
requests): logged-in baseline 827 KB / 9 req → optimized 462 KB / 8 req
(−365 KB, −44%); anonymous 771 KB / 8 req → 402 KB / 7 req (−369 KB, −48%).
The served baselinebundle.min.jsmatched the cleanmasterbuild to the
byte. These supersede the original 787 → 482 KB one-off.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by inspecting the pattern registration code under src/mockup and core/intl-loader.js, then compare the eager-weight and mockup-lazy-* branches to identify one independently scoped change. Done means the selected implementation loads only when needed, preserves widget behavior, and is verified with a production build and the reported payload or emitted-file measurements.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, webpack
- Domain
- build-system, frontend, performance
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100