Cutting a 10-Second Load Time to 2, Without a Rewrite
Situation
First site load took 10-plus seconds to first paint.
The custom front end layered on top of the client’s no-code platform started as a 1,000-line script. It reached me at roughly 2,400 lines. It kept growing after that, and eventually became a single 5,700-line JavaScript file that nobody had ever designed as a whole.
Every role, reviewers, admins, subscribers, coaches, downloaded and parsed all of it before seeing anything on screen. Regardless of which piece of the pie they’d actually use.
Complication
A file that size stops being a convenience and starts being a liability.
Past roughly 5,000 lines, the language server behind syntax highlighting gave up on it. VS Code and Neovim both stopped highlighting the file unless you swapped in a specific language server that could still handle something that size. Until then, the people maintaining the code were reading large stretches of it unhighlighted, plain text, no visual structure to lean on while making changes.
Every change carried the risk of an unrelated regression somewhere else in the file. A single uncaught error would break all custom JavaScript across the entire site. Every user, regardless of role, paid the download and parse cost of code they’d never execute.
And there was no natural place to add new functionality without making the file, and the risk, bigger still.
The obvious fix, a full rewrite, wasn’t available. The platform runs live, for paying users, and can’t go dark for a rearchitecture.
Investigation
Before touching the file, I mapped which code paths each role and each page actually used.
Route-based splitting is the approach most people would reach for first, and it does not survive contact with this platform.
Knack lets you grant roles access to pages. It does not give you role-based access control, security rules, or display rules. Any of that has to be built in JavaScript. So pages are shared across roles, and one role often gets a different version of the script than another, conditionally showing, hiding, or creating information on the same page.
Those boundaries already existed in the permissions model and the page structure. They just weren’t reflected in the code yet. Splitting along them wasn’t an arbitrary call, it matched how the app already behaved.
Resolution
I decomposed the monolith into 112 organized modules, split by domain (events/, knack/, lib/, plugins/, roles/), in a single coordinated release rather than an incremental drift that risks losing track of what’s been moved where.
Two decisions did most of the work:
- Role-scoped lazy loading. Each role folder has its own init file that acts as a registry for that role: it registers the role’s event listeners right after login, asynchronously, so nothing blocks initial content from rendering. A shared core carries the 28 modules every role needs. Past that, a reviewer pulls 16 and a professional 6.
- Page-level just-in-time loading. The event listeners load upfront, but the modules themselves don’t. A given page’s actual module only loads when that page renders and its listener fires, not all at once at login.
Shipping a change this size against a live platform came down to being able to rehearse it properly. The application loads its script from my own server, so I can point an environment at a staging folder and run the entire decomposition against real behaviour before production ever sees it. That is the same deployment pipeline that serves the platform’s assets, and it is the reason a single release covering the whole file was a reasonable thing to attempt.
Results
- Initial paint dropped from 10-plus seconds to 2, measured with Lighthouse and the browser network tab. Role- and page-scoped loading means the browser no longer parses code the current user won’t touch before showing anything.
- 5,700 lines in one file became 112 organized modules, each scoped to a clear domain, and each back under the line count where editors highlight syntax normally again.
- Bundle size now scales with role and page, from a shared core of 28 modules up to what the heaviest role actually needs, instead of every role and every page paying for the whole application upfront.
- Time to ship dropped. A grep now finds the exact file and function for the task at hand; utilities are grouped by topic and imports point straight to where a function lives, instead of a search through one 5,700-line file.
- Zero downtime during the transition. The decomposition shipped as a single release against a live platform, after running in full in staging.