plone / plone/mockup

make mockup pattern only load the heaving parts when needed

Open
#1,618 1 comment 1 reaction 0 assignees View on GitHub

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 ships Intl.DateTimeFormat natively.

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.js served (295,207 B) matched the clean
master build to the byte, and the eager svelte runtime chunk (~172 KB,
chunk 68551) 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
    separate mockup-lazy-integration branch that combines the five widget
    lazifications with a different, more aggressive Intl change (it removes
    core/intl-loader.js outright); 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.js still loads on every logged-in page via
    patternslib pat-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 — master 5,075 files / 3,576 JS chunks / 288 KB
    bundle.min.js vs eager-weight 1,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 formatjs intl-datetimeformat locale files; the
    svelte/icons/Intl removals match the commit messages on
    eager-weight; select2 ↔ relateditems share commit
    406a25742; pat-display-time does await import("moment").
  • Re-measured live against a running Plone site, each build compiled into
    plone.staticresources and 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 baseline bundle.min.js matched the clean master build to the
    byte. These supersede the original 787 → 482 KB one-off.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.